PKU C++程序設計實習 學習筆記5 文件操作和模板

第七章 文件操作和模板

7.1 文件操作

1.數據的層次

  • 位 bit
  • 字節 byte
  • 域/記錄:
數據在計算機中保存時具有一定的層次化結構。
數據在計算機實質上被保存的就是一個個0,1的比特位。它是每位這樣存放的。但是我們具體如果去處理每個比特位的話,那麼很多時候數據在構建的時候會非常繁瑣,並且具有很強的不規律性。
所以我們進一步就把這8個比特位構成的稱之爲字節。那麼每一個byte對應描述了一定的內容,
而這些各個字節組成的一些具體的內容,我們又把它稱之爲域或者記錄。

將所有記錄順序地寫入一個文件—>順序文件

2.文件和流

順序文件 — 一個有限字符構成的順序字符流

C++標準庫中: ifstream, ofstream和fstream共3個類,用於文件操作 — 統稱爲文件流類

3.文件操作

使用/創建文件的基本流程

4.建立順序文件


  • 也可以先創建 ofstream對象, 再用 open函數 打開
    ofstream fout;
    fout.open( “test.out”, ios::out|ios::binary );
  • 判斷打開是否成功:
    if(!fout) { cerr << “File open error!”<<endl; }
  • 文件名可以給出絕對路徑, 也可以給相對路徑    沒有交代路徑信息, 就是在當前文件夾下找文件

5.文件的讀寫指針

對於輸入文件,有一個讀指針
對於輸出文件, 有一個寫指針
對於輸入輸出文件, 有一個讀寫指針
標識文件操作的當前位置,該指針在哪裏,讀寫操作就在哪裏進行

ofstream fout(“a1.out”, ios::app);
long location = fout.tellp(); //取得寫指針的位置
location = 10L;
fout.seekp(location); // 將寫指針移動到第10個字節處
fout.seekp(location, ios::beg); //從頭數location
fout.seekp(location, ios::cur); //從當前位置數location
fout.seekp(location, ios::end); //從尾部數location
location 可以爲負值

6.二進制文件讀寫

Note -- 文本文件/二進制文件打開文件的區別:
• 在Unix/Linux下, 二者一致, 沒有區別;
• 在Windows下, 文本文件是以 “\r\n”作爲換行符
    讀出時, 系統會將0x0d0a只讀入0x0a
    寫入時, 對於0x0a系統會自動添加0x0d
ifstream對象的gcount函數可以得到剛纔讀入的字節數。

7.顯式關閉文件

ifstream fin(“test.dat”, ios::in);
fin.close();
ofstream fout(“test.dat”, ios::out);
fout.close();


7.2 函數模板

1.泛型程序設計(Generic Programming)

算法實現時不指定具體要操作的數據的類型

泛型——算法實現一遍,適用於多種數據結構

優勢: 減少重複代碼的編寫

兩種類型

  • 函數模板
  • 類模板
與“抽象、封裝、繼承、多態”並列


2.函數模板

template<class 類型參數1, class 類型參數2, … >
返回值類型 模板名 (形參表)
{
  函數體
}
例子,交換兩個變量值的函數模板

template <class T>
void Swap(T & x,T & y)
{
  T tmp = x;
  x = y;
  y = tmp;
}
int main(){
  int n = 1, m = 2;
  Swap(n, m); //編譯器自動生成 void Swap(int &, int &)函數
  double f = 1.2, g = 2.3;
  Swap(f, g); //編譯器自動生成 void Swap(double &, double &)函數
  return 0;
}
函數模板可以重載,只要它們的形參表不同即可

下面兩個模板可以同時存在:

template<class T1, class T2>
 void print(T1 arg1, T2 arg2)
 {
   cout<< arg1 << " "<< arg2<<endl;
 }
 template<class T>
 void print(T arg1, T arg2)
 {
   cout<< arg1 << " "<< arg2<<endl;
 }
C++編譯器遵循以下優先順序:

  • Step 1: 先找參數完全匹配普通函數(非由模板實例化而得的函數)
  • Step 2: 再找參數完全匹配模板函數
  • Step 3: 再找實參經過自動類型轉換後能夠匹配的普通函數
  • Step 4: 上面的都找不到,則報錯
例:函數模板調用順序

template <class T>
T Max(T a, T b)
{
  cout << "Template Max 1" <<endl;
  return 0;
}
template <class T, class T2>
T Max(T a, T2 b)
{
  cout << "Template Max 2" <<endl;
  return 0;
}
double Max(double a, double b){
  cout << "MyMax" << endl;
  return 0;
}
int main()
{
  int i=4, j=5;
  Max(1.2,3.4); //調用Max(double, double)函數
  Max(i, j); //調用第一個T Max(T a, T b)模板生成的函數
  Max(1.2, 3); //調用第二個T Max(T a, T2 b)模板生成的函數
  return 0;
}
賦值兼容原則引起函數模板中類型參數的二義性

template<class T>
T myFunction(T arg1, T arg2)
{
  cout<<arg1<<“ ”<<arg2<<“\n”;
  return arg1;
}
…
myFunction(5, 7); //ok: replace T with int
myFunction(5.8, 8.4); //ok: replace T with double
myFunction(5, 8.4); //error: replace T with int or double? 二義性
可以在函數模板中使用多個類型參數, 可以避免二義性

template<class T1, class T2>
T1 myFunction( T1 arg1, T2 arg2)
{
  cout<<arg1<<“ ”<<arg2<<“\n”;
  return arg1;
}
…
myFunction(5, 7); //ok:replace T1 and T2 with int
myFunction(5.8, 8.4); //ok: replace T1 and T2 with double
myFunction(5, 8.4); //ok: replace T1 with int, T2 with double

7.3 類模板

類模板的定義

C++的類模板的寫法如下:
template <類型參數表>
class 類模板名
{
  成員函數和成員變量
};
類型參數表的寫法就是:
class 類型參數1, class 類型參數2, …
類模板裏的成員函數,若在類模板外面定義時:
template <型參數表>
返回值類型 <span style="color:#3333ff;">類模板名<類型參數名列表></span>::成員函數名(參數表)
{
  ……
}
用類模板定義對象的寫法如下:
類模板名 <真實類型參數表> 對象名(構造函數實際參數表);
如果類模板有無參構造函數, 那麼也可以只寫:
類模板名 <真實類型參數表> 對象名;
示例
//Pair類模板:
template <class T1, class T2>
class Pair{
 public:
<span style="white-space:pre">	</span>T1 key; //關鍵字
<span style="white-space:pre">	</span>T2 value; //值
<span style="white-space:pre">	</span>Pair(T1 k,T2 v):key(k),value(v) { };
<span style="white-space:pre">	</span>bool operator < (const Pair<T1,T2> & p) const;
};
template<class T1,class T2>
bool Pair<T1,T2>::operator<( const Pair<T1, T2> & p) const
//Pair的成員函數 operator <
{ return key < p.key; } 
//Pair類模板的使用:
int main()
{
  Pair<string, int> student("Tom",19);
  //實例化出一個類 Pair<string, int>
  cout << student.key << " " << student.value;
  return 0;
}
輸出結果:
Tom 19

使用類模板聲明對象

編譯器由類模板生成類的過程叫類模板的實例化
  • 編譯器自動用具體的數據類型,替換類模板中的類型參數,生成模板類的代碼

由類模板實例化得到的類叫模板類

  • 爲類型參數指定的數據類型不同, 得到的模板類不同

同一個類模板的兩個模板類是不兼容的
Pair<string, int> * p;
Pair<string, double> a;
p = & a; //wrong

函數模版作爲類模板成員

#include <iostream>
using namespace std;
template <class T>
class A{
 public:
   template<class T2>
   void Func(T2 t) { cout << t; } //成員函數模板
};
int main()
{
  A<int> a;
  a.Func('K'); //成員函數模板 Func被實例化
  return 0;
}
程序輸出:
K
要注意,類模板的類型參數,和成員函數模板的類型參數,是不能一致的。

若函數模板改爲template <class T>void Func(T t){cout<<t}將報錯 “declaration of ‘class T’shadows template parm ‘class T’ ”

類模板與非類型參數

類模板的參數聲明中可以包括非類型參數,如:
template <class T, int elementsNumber>
  • 非類型參數:用來說明類模板中的屬性
  • 類型參數:用來說明類模板中的屬性類型,成員操作的參數類型和返回值類型
類模板的 “<類型參數表>” 中可以出現非類型參數:
template <class T, int size>
class CArray{
 T array[size];
public:
 void Print( )
 {
   for(int i = 0; i < size; ++i)
   cout << array[i] << endl;
 }
};
CArray<double, 40> a2;
CArray<int, 50> a3;
注意:
CArray<int,40>和CArray<int,50>完全是兩個類
這兩個類的對象之間不能互相賦值

類模板與繼承


模板類:類模板中類型/非類型參數示例化後的類

(1) 類模板從類模板派生
template <class T1, class T2>
class A {
  T1 v1; T2 v2;
};
template <class T1, class T2>
class B:public A<T2,T1>{
  T1 v3; T2 v4;
};
template <class T>
class C:<strong>public</strong> B<T,T>{
  T v5;
};
int main(){
  B<int, double> obj1;
  C<int> obj2;
  return 0;
<pre name="code" class="cpp">};
obj1實例化:
class B<int, double>:public A<double, int>{ }
  int v3; double v4;
};
class A<double, int> {
  double v1; int v2;
};
(2) 類模板從模板類派生

也就是說,將一個具體的類從類模板裏實例化,從這個實例化後的模板類裏面,去派生一個類模板。
template <class T1, class T2>
class A { T1 v1; T2 v2; };
template <class T>
class B:public A<int, double> { T v; };
int main() {  B<char> obj1;  return 0; }

自動生成兩個模板類:A<int, double>和B<char>

(3) 類模板從普通類派生

class A { int v1; };

template <class T>
class B:public A { T v; };

int main() {
  B<char> obj1;
  return 0;
}


(4)普通類從模板類派生
template <class T>class A { T v1; int n; };class B:public A<int> { double v; };int main() {  B obj1;  return 0;}

7.4 string類

1.string類

  • string 類 是一個模板類, 它的定義如下:  typedef basic_string<char> string;
  • 使用string類要包含頭文件 <string>
  • string對象的初始化:string s1("Hello"); // 一個參數的構造函數     字符串string s2(8, ‘x’); //兩個參數的構造函數         整數+字符string month = “March”; 
  • 不提供以字符和整數爲參數的構造函數
  • 錯誤的初始化方法:string error1 = ‘c’; // 錯string error2(‘u’); // 錯string error3 = 22; // 錯string error4(8); // 錯
  • 可以將字符賦值給string對象string s;s = ‘n’;
  • 構造的string太長而無法表達,會拋出length_error異常
  • string 對象的長度用成員函數 length()讀取;    string s("hello");    cout << s.length() << endl;
  • string 支持流讀取運算符    string stringObject;    cin >> stringObject;
  • string 支持getline函數    string s;    getline(cin, s); 

2.string的賦值和連接

  • 用 ‘=’ 賦值    string s1("cat"), s2;    s2 = s1;
  • 用 assign成員函數複製    string s1("cat"), s3;    s3.assign(s1);
  • 用 assign成員函數部分複製    string s1("catpig"), s3;    s3.assign(s1, 1, 3);  //從s1 中下標爲1的字符開始複製3個字符給s3
  • 單個字符複製    s2[5] = s1[3] = ‘a’;
  • 逐個訪問string對象中的字符    string s1("Hello");    for(int i=0; i<s1.length(); i++)      cout << s1.at(i) << endl; 成員函數at會做範圍檢查, 如果超出範圍, 會拋出out_of_range異常, 而下標運算符不做範圍檢查
  • 用 + 運算符連接字符串    string s1("good "), s2("morning! ");    s1 += s2;    cout << s1;
  • 用成員函數 append 連接字符串    string s1("good "), s2("morning! ");    s1.append(s2);    cout << s1;    s2.append(s1, 3, s1.size());    //s1.size(), s1字符數     開始位置的下標+個數    cout << s2;   //下標爲3開始, s1.size()個字符//如果字符串內沒有足夠字符, 則複製到字符串最後一個字符

3.比較string

  • 用關係運算符比較string的大小    == , >, >=, <, <=, !=    返回值都是bool類型, 成立返回true, 否則返回false
  • 用成員函數compare比較string的大小

4.子串

  • 成員函數 substr()    string s1("hello world"), s2;    s2 = s1.substr(4,5); //下標4開始5個字符        開始位置的下標+個數

5.交換string

  • 成員函數 swap()    string s1("hello world"), s2("really");    s1.swap(s2); 

6.string的特性

  • 成員函數 capacity()                    返回無需增加內存即可存放的字符數
  • 成員函數 maximum_size()        返回string對象可存放的最大字符數
  • 成員函數 length()和size()相同  返回字符串的大小/長度
  • 成員函數 empty()                        返回string對象是否爲空
  • 成員函數 resize()                        改變string對象的長度

7.尋找string中的字符

  • 成員函數 find()    string s1("hello world");    s1.find("lo");    //在s1中從前向後查找 “lo” 第一次出現的地方,如果找到, 返回 “lo”開始的位置, 即 l 所在的位置下標    //如果找不到, 返回 string::npos (string中定義的靜態常量)
  • 成員函數 rfind()    string s1("hello world");    s1.rfind("lo");    //在s1中從後向前查找 “lo” 第一次出現的地方,如果找到, 返回 “lo”開始的位置, 即 l 所在的位置下標    //如果找不到, 返回 string::npos
  • 成員函數 find_first_of()    string s1("hello world");    s1.find_first_of(“abcd");    //在s1中從前向後查找 “abcd” 中任何一個字符第一次出現的地方,如果找到, 返回找到字母的位置; 如果找不到, 返回 string::npos
  • 成員函數 find_last_of()    string s1("hello world");    s1.find_last_of(“abcd");    //在s1中查找 “abcd” 中任何一個字符最後一次出現的地方,如果找到, 返回找到字母的位置; 如果找不到, 返回 string::npos
  • 例子
string s1("hello worlld");
 cout << s1.find("ll") << endl;                             <span style="font-family: Arial, Helvetica, sans-serif;">2</span>
 cout << s1.find("abc") << endl;                            <span style="font-family: Arial, Helvetica, sans-serif;">4294967295   (常量)</span>
 cout << s1.rfind("ll") << endl;                            <span style="font-family: Arial, Helvetica, sans-serif;">9</span>
 cout << s1.rfind("abc") << endl;                           <span style="font-family: Arial, Helvetica, sans-serif;">4294967295</span>
 cout << s1.find_first_of("abcde") << endl;                 <span style="font-family: Arial, Helvetica, sans-serif;">1</span>
 cout << s1.find_first_of("abc") << endl;                   <span style="font-family: Arial, Helvetica, sans-serif;">4294967295</span>
 cout << s1.find_last_of("abcde") << endl;                  <span style="font-family: Arial, Helvetica, sans-serif;">11</span>
 cout << s1.find_last_of("abc") << endl;                    <span style="font-family: Arial, Helvetica, sans-serif;">4294967295</span>
 cout << s1.find_first_not_of("abcde") << endl;             <span style="font-family: Arial, Helvetica, sans-serif;">0</span>
 cout << s1.find_first_not_of("hello world") << endl;       <span style="font-family: Arial, Helvetica, sans-serif;">4294967295</span>
 cout << s1.find_last_not_of("abcde") << endl;              <span style="font-family: Arial, Helvetica, sans-serif;">10</span>
 cout << s1.find_last_not_of("hello world") << endl;        <span style="font-family: Arial, Helvetica, sans-serif;">4294967295</span>

8.替換string中的字符

  • 成員函數erase()
        string s1("hello worlld");
        s1.erase(5);  //保留長度爲5
  • 成員函數 find()
        string s1("hello worlld");
        cout << s1.find("ll", 1) << endl;   //從下標1開始查找
  • 成員函數 replace()
        string s1("hello world");
        s1.replace(2,3, “haha");  //將s1中下標2 開始的3個字符換成 “haha”
        string s1("hello world");
        s1.replace(2,3, "haha", 1,2);  //將s1中下標2 開始的3個字符換成 “haha” 中下標1開始的2個字符

9.在string中插入字符

  • 成員函數 insert()
        string s1(“hello world”);
        string s2(“show insert”);
        s1.insert(5, s2);     // 將s2插入s1下標5的位置
        s1.insert(2, s2, 5, 3);    //將s2中下標5開始的3個字符插入s1下標2的位置

10.轉換成C語言式char *字符串

  • 成員函數 c_str()
        string s1("hello world");
        printf("%s\n", s1.c_str());// s1.c_str() 返回傳統的const char * 類型字符串且該字符串以 ‘\0’ 結尾
  • 成員函數data()
        string s1("hello world");
        const char * p1=s1.data();
        for(int i=0; i<s1.length(); i++)
          printf("%c",*(p1+i));
    //s1.data() 返回一個char * 類型的字符串,對s1 的修改可能會使p1出錯。
  • 成員函數copy()string s1("hello world");
        int len = s1.length();
        char * p2 = new char[len+1];
        s1.copy(p2, 5, 0);
        p2[5]=0;
        cout << p2 << endl;
    // s1.copy(p2, 5, 0) 從s1的下標0的字符開始,製作一個最長5個字符長度的字符串副本並將其賦值給p2,返回值表明實際複製字符串的長度

7.5 輸入和輸出

1.與輸入輸出流操作相關的類


istream是用於輸入的流類,cin就是該類的對象。

ostream是用於輸出的流類,cout就是該類的對象。

ifstream是用於從文件讀取數據的類。

ofstream是用於向文件寫入數據的類。

iostream是既能用於輸入,又能用於輸出的類。

fstream 是既能從文件讀取數據,又能向文件寫入數據的類。

2.標準流對象

輸入流對象: cin 與標準輸入設備相連

輸出流對象:cout 與標準輸出設備相連
                        cerr 與標準錯誤輸出設備相連
                        clog 與標準錯誤輸出設備相連

cin對應於標準輸入流,用於從鍵盤讀取數據,也可以被重定向爲從文件中讀取數據。

cout對應於標準輸出流,用於向屏幕輸出數據,也可以被重定向爲向文件寫入數據。

cerr對應於標準錯誤輸出流,用於向屏幕輸出出錯信息,clog對應於標準錯誤輸出流,用於向屏幕輸出出錯信息,

cerr和clog的區別在於cerr不使用緩衝區,直接向顯示器輸出信息;而輸出到clog中的信息先會被存放在緩衝區,緩衝區滿或者刷新時才輸出到屏幕。

freopen("test.txt","w",stdout); //將標準輸出重定向到 test.txt文件

3.判斷輸入流結束

可以用如下方法判輸入流結束:

 int x;
 while(cin>>x){
 …..
 }
 return 0;

istream &operator >>(int a)
{
 …….
 return *this ;
}

這個表達式它的返回值雖然是cin 但是在istream裏面有一個強制類型轉換運算符的重載就能夠把這個cin對象強制轉換成類型的值。所以當輸入流結束的時候,這個cin被轉換出來的布爾類型的值就是false。

如果是從文件輸入,比如前面有freopen(“some.txt”,”r”,stdin);那麼,讀到文件尾部,輸入流就算結束

如果從鍵盤輸入,則在單獨一行輸入Ctrl+Z代表輸入流結束 

4.istream類的成員函數

istream & getline(char * buf, int bufSize);
從輸入流中讀取bufSize-1個字符到緩衝區buf,或讀到碰到‘\n’爲止(哪個先到算哪個)。
istream & getline(char * buf, int bufSize,char delim);
從輸入流中讀取bufSize-1個字符到緩衝區buf,或讀到碰到delim字符爲止(哪個先到算哪個)。

兩個函數都會自動在buf中讀入數據的結尾添加\0’。
輸入流裏的‘\n’或delim都不會被讀入buf,但會被從輸入流中取走。
如果輸入流中‘\n’或delim之前的字符個數達到或超過了bufSize個,就導致讀入出錯,其結果就是:雖然本次讀入已經完成,但是之後的讀入就都會失敗了。

可以用 if(!cin.getline(…)) 判斷輸入是否結束

  • bool eof(); 判斷輸入流是否結束
  • int peek(); 返回下一個字符,但不從流中去掉.
  • istream & putback(char c); 將字符ch放回輸入流
  • istream & ignore( int nCount = 1, int delim = EOF );   從流中刪掉最多nCount個字符,遇到EOF時結束。

5.流操縱算子

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章