C++自修入門實境秀、C++ Primer 5版研讀秀 85/ ~12.1. Dynamic Memory and Smart Pointers...
頁473
指標經過檢查的指標類別(有檢查check機制的指標類別)
第85集開始
4:31:42 什麼叫做「指標類別」也不過就是它的成員函式支援/提供了如同指標一般的運算,如StrBlobPtr中的incr、deref二個成員函式就是模擬/模仿內建型別(builtin type)指標的「++、*」運算子的功能,或者說,利用了「++、*」運算子的功能來達到所謂的指標類別該提供的諸如遞增(推進)和解參考的操作/運算。只要有樣的成員函式,大概就可以視之為指標類別了。4:42:30
所以類別叫/是「抽象資料結構」只要其資料(此資料是指類別內的內容、內容物、所含物)結構符合某些特性或具備哪些功能的,就屬哪一種類別了。
為了示範weak_ptr是幹什麼用的、用weak_ptr有什麼好處,我們可以為我們曾定義的StrBlob類別定義一個與它相伴的指標類別(companion pointer class)。我們將這個指標類別,命名為StrBlobPtr,它將會儲存一個由StrBlob的data這個資料成員來初始化的weak_ptr,而這個weak_ptr則會指向StrBlob中data這個shared_ptr型別的資料成員指向的vector;也就是與StrBlob中的這個shared_ptr(名為data)共享其所指物件vector。
頁474
改中文版作文:
因為是使用了weak_ptr,而不是用shared_ptr,所以不會去影響到StrBlob類別物件指向的那個vector的生命週期。也可以由此防止使用者試圖去存取不再存在的vector。因為不再存在的物件可以藉由weak_ptr的lock成員函式來代為檢查。14:00 20:51
StrBlobPtr會有兩個資料成員:一個是wptr,它要嘛是一個null值(空指標),要嘛就是指向StrBlob中vector的weak_ptr ;另一個則是curr,它是這個vector物件的索引值,代表目前所在的元素。我們的這個指標類別StrBlobPtr就像它的伙伴類別(companion classStrBlobPtr的StrBlob伴隨類別companion pointer class;StrBlob則是StrBlobPtr的伙伴類別)StrBlob,也會有一個check成員函式來確保解參考StrBlobPtr是安全的:49:11 第85集 2:51:00
StrBlobPtr的定義
// 當企圖存取一個不存在的vector元素時, StrBlobPtr就會丟出一個例外:
class StrBlobPtr {57:30
public:
StrBlobPtr () : curr(0){ }//第1個建構器(也是預設建構器(default constructor)——沒有引數)
StrBlobPtr(StrBlob &a, size_t sz = 0):
wptr(a.data), curr(sz){ } //第2個建構器
std::string& deref() const;
StrBlobPtr& incr() ; // 前缀遞增++版本(prefix version)
private:
//只要通過檢查,check就會回傳一個指向vector的shared_ptr
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string&) const;
// wptr儲存一個weak_ptr,利用它來作為一個底層vector可能已摧毁的指示(指標;這個智慧指標不是用來管控其所指物的生死,而是用來檢查其所指物件是否還存在)
// wptr store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // curr是用來指示vector中目前元素的位置// current position within the array——應是英文版筆誤!
};
1:41:38
預設的建構器會產生一個null的StrBlobPtr。它的建構器初始器串列(§ 7.1.4,頁265)會明確地將curr初始化為零,並隱含地將wptr初始化為一個null的weak_ptr。第二個建構器則接受對StrBlob的一個參考及一個選擇性的索引值。這個建構器將wptr初始化為指向某個StrBlob物件裡頭的shared_ptr所指的vector,並用sz的值來將curr初始化。這裡,我們使用了一個預設引數「0」(§6.5.1,頁236)來將curr預設初始化為代表vector中的第一個元素。我們將看到,StrBlob的end成員函式會用得上sz這個參數。
值得注意的是,我們無法將一個StrBlobPtr繫結至一個const StrBlob物件。這種限制由於StrBlobPtr的第二個建構器接受的引數是對型別為非const的StrBlob物件的一個參考,而不是對const的StrBlob物件的一個參考。
StrBlobPtr的check成員函式也與StrBlob中的那個不同,因為它必須撿查它所指的vector是否仍然存在: 2:45:30 臉書428集開始
// StrBlobPtr的成員函式check會傳回一個shared_ptr的智慧指標,這個指標指向的是元素型別為string的vector物件2:55:10
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock();// 用ret區域變數來記下Weak_ptr這個型別StrBlobPtr資料成員wptr,它叫用StrBlobPtr的成員函式lock回傳的結果,這個結果是一個shared_ptr型別的指標;用這個表達式(運算式express)來以測度wptr所指向的vector物件是否還存在。wptr是由StrBlob資料成員data來初始化的,也就是與shared_ptr型別的data指向的是同一個物件,在此這個物件是vector
if(!ret) //如果傳回來的指標為null值,便等於「0」,條件式就會成立(在C++中true=1,false=0)
throw std::runtime_error("unbound StrBlobPtr");//要用「runtime_error」要記得「#include <stdexcept>」。「"unbound StrBlobPtr"」表示調用這個check的StrBlobPtr型別物件(它是一個指標型別)並沒有指向任何物件(在這裡是vector裡的元素)——沒有和任何物件繫結(bind)在一塊;而StrBlob的資料成員、型別為shared_ptr的data,指向的則是那個vector
if(i >= ret->size()) 3:31:20
throw std::out_of_range(msg);
return ret; // 只要通過了以上兩個if條件式的檢驗,就可以放心回傳那個由wptr調用lock成員函式回傳的 shared_ptr,這個shared_ptr和wptr是指向同一個的vector//要用「out_of_range」也要記得「#include <stdexcept>」。「ret」應即「return」的縮寫
}
3:12:00 第85集
《泰晤士報》(The Times)為什麼不翻成《泰姆士報》?
聲音變化的關係 ㄨwu~→ㄇm、ㄈf?→ㄏ?
九陽神功第3招字形結構兼音義、聲音變化的關係
頁475
改中文版作文:
4:26:54 4:30:55 4:37:11 4:48:27
4:55:00 變動Visual Studio的資料成員(data member——fields)字型顏色設定5:7:30
因為weak_ptr並不會對其相應的shared_ptr之參考計數動手腳或插一手(participate),所以這個StrBlobPtr指向的 vector有可能已被Shared_ptr刪除。5:9:20只要這個vector已不存在,那麼在weak_ptr物件wptr上調用lock成員函式就會回傳一個null(空的指標、空指標)。如此一來(In this case,),只要是參考到這個vector的任何操作都會失敗。若真是這樣,就丟出一個例外情形,中止呼叫check端的程式。如果lock回傳的並不是空指標,check就會接著檢驗傳給它的索引值引數:如果那個引數值沒有問題,不在容器之外,check就會回傳它從lock獲得的shared_ptr,並順利(正常;前二者均觸發了例外情形!)結束check區塊主體的執行。
指標運算
5:21:30 這就關係到為什麼StrBlobPtr是一個指標類別了,因為其類別具備了指標的運算性能、具有指標的屬性
我們會在第14章學到如何定義我們自己的運算子。而現在,我們則透過定義名為deref與 incr這兩個成員函式來對StrBlobPtr這個型別的指標物件進行解參考和遞增運算。
deref成員函式先呼叫check來確認對vector的使用是安全的,而curr這個作為該vector索引值的資料成員(data member;field)的值也沒超出vector的大小:
std::strings StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p):解參考p後得到的是check回傳的shared_ptr指標物件p所指向那個的 vector
}
如果check通過,沒有丟出例外,p就會接收check回傳的值,這個值就是一個shared_ptr,這個shared_ptr指向這個StrBlobPtr物件所指的vector(也就是這個vector是由這個shared_ptr與這個調用deref的StrBlobPtr型別物件共享。)運算式 (*p)[curr]會解參考那個shared_ptr來取得vector,並使用下標運算子來擷取並回傳位於 curr 這個索引值位置上的元素。
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
/*真正「推進」元素是在這行,不是在incr(),incr()只是「推進」索引值而已。再在此deref()先檢查
索引值curr有效否,有效才「真的」「推進」(其實是對vector的「下標運算」!)
故反而是在解參考(即deref())時才「推進」元素(實即對vector下標爾),而不是在incr()就推進了
真正有「推進」的,只有索引值,而元素並未被「推進」,只是vector被下標(subscript)而已
第85集 4:5:00 */
}
incr成員函式也會呼叫check 來先作檢查:
//前綴:回傳指向遞增過的物件的一個參考。英文版這樣說會誤導人,其實這裡StrBlobPtr並沒有真的遞增(推進)過,只是它的資料成員curr遞增而已。然後再用這個curr作為索引值來對StrBlobPtr所指向的vector下標,如是而已(並未遞增——推進!)
StrBlobPtr& StrBlobPtr: :incr ()
{
//如果curr已經指向超過容器尾端的地方,就無法遞增它
check(curr, "increment past end of StrBlobPtr");
++ curr;//作為模擬推進目前元素位置用的數據——其實只有作為下標運算子([]運算子,subscript operator)的運算元索引值遞增(加1)而已
return *this;
}
5:38:30
StrBlobPtr& StrBlobPtr::incr()
{
// if curr already points past the end of the container, can't increment it
/*若如課本,只寫「=」後面的表達式,那麼編譯器會出現這樣的警告訊息:
Severity Code Description Project File Line Suppression State
Warning C26444 Avoid unnamed objects with custom construction and destruction (es.84). prog1 V:\PROGRAMMING\C++\OSCARSUN72\PROG1\PROG1\STRBLOB.CPP 85
*/
//auto sp = check(curr, "increment past end of StrBlobPtr");
shared_ptr<vector<string>> sp = check(curr, "increment past end of StrBlobPtr");
++curr; // advance the current state
return *this;
}
5:53:55 5:56:00
因為有了StrBlobPrt,我們也可以給賦予我們的StrBlob類別提供begin和end這兩項運算——即兩個成員函式支援這樣的運算/操作。begin會回傳一個指向 StrBlob中的第一個元素的StrBlobPtr,而end則是回傳一個指向StrBlob的最後一個元素的後面那個位置的StrBlobPtr。要完善StrBlob與StrBlobPtr這兩個伙伴類別的關係,除了這些設計之外,因為StrBlobPtr會用到 StrBlob 的私有的(private:)資料成員 data,我們就必須指定 StrBlobPtr 是 StrBlob 的一個 friend ( § 7.3.4 ,頁279)才行:
class StrBlob {
friend class StrBlobPtr;
//其他的成員內容跟§12.1.1(頁456)中一樣
StrBlobPtr begin();// 回傳StrBlobPtr來指向StrBlob中vector的第一個元素
StrBlobPtr end() ;// 回傳StrBlobPtr來指向StrBlob中vector的超出最後元素一個位置的地方
};
//這些成員可以先宣告,但必須在StrStrBlob和StrStrBlobPtr定義好之後才能加以定義
StrBlobPtr StrBlob::begin(){ return StrBlobPtr(*this);}
StrBlobPtr StrBlob::end{){ return StrBlobPtr(*this, data->size()); }
頁476
練習12.19
自訂您版本的StrBlobPtr,並且使用適當的friend宣告、和定義begin、end這兩個成員函式來更新您先前那個StrBlob類別的定義。
參見前面練習12.2
標頭檔
#ifndef STRBLOB_H
#define STRBLOB_H
#include<string>
#include<vector>
#include<memory>
//class StrBlobPtr;//可以重複宣告,卻不能重複定義;若無此行,則StrBlob中成員函式用 到StrBlobPtr都會在編譯時期出錯「use of undefined type 'StrBlobPtr'」
class StrBlob
{
friend class StrBlobPtr;//頁269-270,279-280;不加「class」的就會當作函式編譯
public:
typedef std::vector<std::string>::size_type size_type; //以型別別名定義型別成員(type member)
StrBlob(); //建構器
StrBlob(std::initializer_list<std::string> il); //帶了一個initializer_list<string>參數的建構器
size_type size() const { return data->size(); } //常值的const成員函式
bool empty() const { return data->empty(); } //常值的const成員函式
// add and remove elements
void push_back(const std::string& t) {
data->push_back(t);
}
void pop_back();
// element access
std::string& front();
std::string& back();
// return StrBlobPtr to the first and one past the last elements
StrBlobPtr begin();
StrBlobPtr end();
private:
std::shared_ptr<std::vector<std::string>> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string& msg) const;
};
//伙伴類別似乎是要放在同一個標頭檔中!!否則編譯(建置)時會出錯。 https://docs.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2027?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev16.query%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(C2027)%26rd%3Dtrue%26f%3D255%26MSPPError%3D-2147217396&view=vs-2019
//伙伴類別(companion class,這裡是companion pointer class)是否就要放在同一個標頭檔中呢?!否則就會出錯
class StrBlobPtr
{
friend class StrBlob;
public:
StrBlobPtr() : curr(0) {}//第1個建構器(也是預設建構器(default constructor)——沒有引數)
StrBlobPtr(StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}//第2個建構器
std::string& deref() const;
StrBlobPtr& incr(); // 前缀版本(prefix version)
StrBlobPtr& decr(); // 前缀版本(prefix version)
bool isEnd();//用來判斷已到末尾元素
private:
//如果檢查成功,check會回傳一個shared_ptr指向vector
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string&) const;
// wptr儲存一個weak_ptr,利用它來作為一個底層vector可能已摧毁的指示
//(指標;這個智慧指標不是用來管控其所指物的生死,而是用來檢查其所指物件是否還存在)
// store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // curr是用來指示vector中目前元素的位置
// current position within the array——應是英文版筆誤!
};
#endif // !STRBLOB_H
源碼檔(source file)
#include"StrBlob.h"
#include<string>
#include<vector>
#include<memory>
#include <stdexcept>
using namespace std;
StrBlob::StrBlob() : data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)) {} //用il來作為make_shared引數,就不是空的vector了
void StrBlob::check(size_type i, const string& msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
string& StrBlob::front()
{
// if the vector is empty, check will throw
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
StrBlobPtr StrBlob::begin()
{
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end()
{
return StrBlobPtr(*this,this->size());
}
//StrBlobPtr StrBlob::begin() {
// return StrBlobPtr(*this);
//}
//
//StrBlobPtr StrBlob::end()
//{
// auto ret = StrBlobPtr(*this, data->size());
// return ret;
//}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
// StrBlobPtr的成員函式check會傳回一個shared_ptr的智慧指標,這個指標指向的是元素型別為string的vector物件2:55:10
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string& msg) const
{
auto ret = wptr.lock();// 用ret區域變數來記下Weak_ptr這個型別StrBlobPtr資料成員wptr,它叫用StrBlobPtr的成員函式lock回傳的結果,這個結果是一個shared_ptr型別的指標;用這個表達式(運算式express)來以測度wptr所指向的vector物件是否還存在。wptr是由StrBlob資料成員data來初始化的,也就是與shared_ptr型別的data指向的是同一個物件,在此這個物件是vector
if (!ret) //如果傳回來的指標為null值,便等於「0」,條件式就會成立(在C++中true=1,false=0)
throw std::runtime_error("unbound StrBlobPtr");//要用「runtime_error」要記得「#include <stdexcept>」。「"unbound StrBlobPtr"」表示調用這個check的StrBlobPtr型別物件(它是一個指標型別)並沒有指向任何物件(在這裡是vector裡的元素)——沒有和任何物件繫結(bind)在一塊;而StrBlob的資料成員、型別為shared_ptr的data,指向的則是那個vector
if (i >= ret->size())
throw std::out_of_range(msg);
if(i<0)
throw std::out_of_range(msg);
return ret; // 只要通過了以上兩個if條件式的檢驗,就可以放心回傳那個由wptr調用lock成員函式回傳的 shared_ptr,這個shared_ptr和wptr是指向同一個的vector//要用「out_of_range」也要記得「#include <stdexcept>」。「ret」應即「return」的縮寫
}
//原課文(中、英文版如下)
/*// 當企圖存取一個不存在的vector元素時, StrBlobPtr就會丟出一個例外:
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string& msg)
const
{
auto ret = wptr.lock(); // is the vector still around?
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret; // otherwise, return a shared_ptr to the vector
}*/
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
/*真正「推進」元素是在這行,不是在incr(),incr()只是「推進」索引值而已。再在此deref()先檢查
索引值curr有效否,有效才「真的」「推進」(其實是對vector的「下標運算」!)
故反而是在解參考(即deref())時才「推進」元素(實即對vector下標爾),而不是在incr()就推進了
真正有「推進」的,只有索引值,而元素並未被「推進」,只是vector被下標(subscript)而已
第85集 4:5:00 */
}
// prefix: return a reference to the incremented object
StrBlobPtr& StrBlobPtr::incr()
{
// if curr already points past the end of the container, can't increment it
/*若如課本,只寫「=」後面的表達式,那麼編譯器會出現這樣的警告訊息:
Severity Code Description Project File Line Suppression State
Warning C26444 Avoid unnamed objects with custom construction and destruction (es.84). prog1 V:\PROGRAMMING\C++\OSCARSUN72\PROG1\PROG1\STRBLOB.CPP 85
*/
//auto sp = check(curr, "increment past end of StrBlobPtr");
shared_ptr<vector<string>> sp = check(curr, "increment past end of StrBlobPtr");
++curr; // advance the current state
return *this;
}
StrBlobPtr& StrBlobPtr::decr()
{
auto p = check(curr-1, "遞減過頭了!");
--curr; //遞減索引值
return *this;
}
bool StrBlobPtr::isEnd()
{
auto p = wptr.lock();
if (p)
{
if (this->curr == p->size())
return true;
}
return false;
}
執行檔、應用程式檔
#include<iostream>
#include"StrBlob.h"
using namespace std;
int main() {
StrBlob stb;
StrBlob stbv{"a","b"};
StrBlob stbv4{"a","b","c","d"};
StrBlobPtr srbp(stbv);//第85集 3:53:00 臉書第428集
cout << srbp.incr().deref() << endl;
cout << stbv4.begin().deref() << endl;
cout << stbv4.end().decr().deref() << endl;
for (StrBlobPtr i = stbv4.begin(); !i.isEnd(); i.incr())
{
cout << i.deref() ;
}
cout << endl;
}
練習12.20
7:32:50
改中文版作文:
寫一個程式來讀取輸入的檔案,一次讀一行到一個StrBlob中,並利用StrBlobPtr來印出那個StrBlob中的每個元素。
參考練習8.4
11:25:40
標頭檔與源碼檔與12.19同這裡只錄執行檔:
#include<iostream>
#include"StrBlob.h"
#include<fstream>
//#include<iostream>
using namespace std;
void readFromFile(const string& fFullName) {
ifstream f(fFullName);
string str;
StrBlob stb;
while (f&&!f.eof())
{
getline(f, str);
stb.push_back(str); //最後一個元素會重複,未詳,俟考!11:29:20
//原來要加以「!f.eof()」這個判斷,讀到檔尾時,才不會又多讀一次。C++自修入門實境秀、C++ Primer 5版研讀秀第86集先講這個!
}
StrBlobPtr stbP(stb);
while (!stbP.isEnd())
{
cout << stbP.deref() << endl;
stbP.incr();
}
}
int main() {//第85集 10:55:30 沒錄到的部分見臉書直播第432集
string fname = "V:\\Programming\\C++\\input.txt";
readFromFile(fname);
}
練習12.21
我們也可以把StrBlobPtr的deref成員函式寫成這樣:
std::string &deref() const
{ return (*check(curr, "dereference past end"))[curr];}
您哪個比較好,為什麼?
原來(課文正文)的版本比較清楚,但若顧慮行內函式(inline function),則可考慮如此寫法:第85集 11:33:30
練習12.22
第85集11:41:00
如果想要讓StrBlobPtr這個類別物件也可以指向常值的(const)StrBlob,那麼它的定義當如何修訂?就請試著定義一個可以用在常值StrBlob上的類別,將之命名作ConstStrBlobPtr。
其實不管是ConstStrBlobPtr或StrBlobPtr都可說是StrBlob的附屬類別。
StrBlobPtr(StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}
改成
StrBlobPtr(const StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}
即可
第85集11:53:00 只要將上式作為第3個建構器即可
標頭檔更動處如下:
class StrBlobPtr
{
friend class StrBlob;
public:
StrBlobPtr() : curr(0) {}//第1個建構器(也是預設建構器(default constructor)——沒有引數)
StrBlobPtr(StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}//第2個建構器
StrBlobPtr(const StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}//第3個建構器,針對常值的StrBlob練習12.22
……
};
頁316
表8.3 : fstream限定的運算
fstream fstrm; 創建一個未繫結的檔案資料流。fstream是定義在fstream標頭中的型別之一。
fstream fstrm(s); 創建一個並開啟名為s的檔案。s可以有包含(have)型別string型別、或可以是指向一個C-style字元字串的指標(§3.5.4,頁122)。這些建構器是explicit的(§7.5.4,頁290)。預設的檔案mode (模式) 取決於fstream的型別。第85集8:9:00
fstream fstrm(s, mode); 就像前一個建構器,但指定了開啟s的mode將s開啟在給定的mode。
fstrm.open(s)
fstrm.open(s, mode) 開啟s所指名的檔案並將那個檔案繫結至fstrm。s可以是一個string或者一個指標,指向一個C-style的字元字串。預 設的檔案mode取決於fstmim的型別。回傳void。
fstrm.close() 關閉fstrm所繫結的檔案。回傳void。
fstrm.is_open() 回傳一個bool,指出與fstrm關聯的檔案是否成功開啟而且尚未被關閉。
參見表8.4 :檔案模式
Table 8.3. fstream-Specific Operations
改中文版作文:
第85集 7:50:00
8.2檔案輸入與輸出
fstream標頭定義了三個型別來支援檔案IO : ifstream來可從一個某個給定的檔案讀取資料, ofstream則可寫入資料到一個給指定的檔案,還有fstream,可以讀寫一個給定的檔案。在§17.5.3(頁763)中,我們會描述如何對使用相同一的檔案來同時進行輸入與輸出。
這些型別所提供的運算與我們之前用在物件cin和cout上的相同。特別是,我們可以使用 IO運算子(<<與 >>)來讀取或寫入檔案,我們也可以使用getline ( §3.2.2,頁87)來讀取一個 ifstream,而在§8.1涵蓋的說明也適用於這些型別討論過的內容也適用於這些型別。
除了它們從iostream型別繼承而來的行為,定義在fstream中的型別新增了成員來管理與資料流關聯的檔案。這些運算,列於表8.3中,可以在fstream、ifstream或ofstream的物件上被呼叫,但在其他IO型別上就不行。
頁317
改中文版作文:
第85集 8:17:00
8.2.1使用檔案資料流物件(File Stream Objects)
當我們想要讀取或寫入一個檔案,我們會先定義一個檔案資料流物件,並將那個物件關聯至該檔案。每個檔案資料流類別都定義了一個叫做open的成員函式,它進行的操作,就是會完成一個作業系統在找檔案和及將之將該檔案開啟的作業中,任何的一切動作,完成這些操作後,以便供程式後續的讀取或寫入。
當我們創建一個檔案資料流,我們可以(選擇性地)提供一個檔案名稱。當我們提供一個檔案名稱,open就會自動被呼叫:
ifstream in(ifile); //建構一個ifstream並開啟ifile所指定的檔案
ofstream out; //建構一個輸出檔案資料流(ofstream)out,而這個out並沒有關聯到與任何檔案的輸出稽案資料流繫結(bind)
這段程式碼將in定義為一個輸入檔案資料流,它以string引數ifile所指名的檔案來作初始化。它而將out定義為一個尚未與檔案關聯(繫結)的輸出檔案資料流。在新標準之下,檔案名稱可以是程式庫的string或C-style的字元陣列(§ 3.5.4)。之前舊版本的程式庫只允許C-style的字元陣列。
在需要iostream&的地方把一個使用fstream用在需要iostream&之處
如我們在§8.1(頁311)提過的,我們可以在一個指定型別的地方,用把一個繼承它的型別的物件來使用在預期被繼承的型別之物件的地方。這個事實就意味著是說,如果一個函式它的參數是接受對某個iostream型別的參考或指標,的函式也能在用對應的 fstream (或sstream)型別上來呼叫。也就是說,如果我們有一個函式需要型別為接受一個ostream&的引數,我們也可以傳給該函式一個ofstream型別的物件來呼叫它,;對於istream&與ifstream來說也是如此一樣的。
舉例來說,我們可以使用§ 7.1.3(頁261)的read和print函式來讀取或寫入具名的檔案。在這個例子中,我們會假設輸入和輸出檔的名稱被當成引數傳入給了 main ( §6.2.5,頁218、219):
ifstream input (argv [1]) ; //開啟銷售的交易記錄檔案
ofstream output (argv [2] ) ; // 開啟輸出檔
Sales_data total; //要存放運行總和的變數
if (read(input, total)) { // 讀取第一筆交易記錄
Sales_data trans; //存放下一筆交易記錄資料的變數
while (read (input, trans)) { // 讀取剩餘的交易記錄
if (total.isbn() == trans.isbn()) // 檢查 isbns
total.combine (trans) ; // 更新運行總和
else {
print(output, total) << endl; // 印出結果
total = trans; //處理下一本書
}
}
print (output, total) << endl; // 印出上一筆交易記錄
}
else //沒有輸入
cerr << "No data?!" << endl;
除了使用改用具名檔案來作為輸出輸入端,這段程式碼與前面§7.1.1(頁255)的加總程式幾乎完全相同。重要關鍵的部分在於對read和print的呼叫。我們可以傳入我們的fstream物件給這些函式,即使它們的參數是 分別定義為istream&和ostream&也都沒關係無妨。
頁318
改中文版作文:
第85集 8:58:14 9:0:39
open和close成員函式
當我們定義一個空的檔案資料流物件,我們可以在之後呼叫open來將一個檔案關聯至該物件:
ifstream in(ifile); //建構一個ifstream並開啟給定的檔案
ofstream out; //沒有與任何檔案關聯的輸出檔案資料流
out.open(ifile + ".copy"); //開啟「ifile + ".copy"」所指定的檔案
如果對open的呼叫失敗,failbit就會被設定(§8.1.2,頁312)。因為對open的呼叫可能會失敗,因此通常比較好的做法是先驗證是否有成功open:
if(out) //檢查open是否成功
// opan成功了,所以我們可以使用這個檔案
這個條件類似於我們在cin上用過的那些。如果open失敗了,這個條件就不成立,也就不會使用到out。
只要一個檔案資料流被開啟,它就會持續關聯至所指定的檔案。確實,在已經開啟的一個檔案資料流上呼叫open是一定會失敗,並會設定failbit。之後對那個檔案資料流的使用也都會出錯。若想要將一個檔案資料流關聯到一個不同的檔案,就必須先關閉現在關聯的檔案。一旦該檔案被關閉,我們就能開啟一個新的來與原有的資料流繫結(關聯): 9:11:12
in.close(); // 關閉檔案
in.open(ifile + "2"); // 開啟另一個檔案
如果open成功,那麼open就會設定資料流的狀態,所以good()會是true。
自動建構與解構
請思考一個程式,其main函式接受一個檔案清單,列出它應該要處理的檔案(§6.2.5,頁218)。這種程式可能會有像下面這樣的一個迴圈:
//對於傳入程式的每個檔案
for (auto p = argv + 1; p != argv + argc; ++p)
{
ifstream input(*p); // 創建 input 並開啟該檔案
if (input)
{ //如果檔案沒有問題,就"process"(處理)這個檔案
process(input);
}
else
cerr << "couldn't open: " + string(*p);
} // input已跑出範疇,並會在每次迭代中被摧毁
每次迭代都會建構一個新的ifstream物件,名為input,並開啟它來讀取所給的檔案。一樣地,我們會先檢查open是否成功。若無誤,我們就將該檔案傳入一個會讀取並處理輸入的函式process。若開啟不成,就印出一個錯誤訊息,然後繼續進行下一個迭代。
因為區域變數input是定義在for的主體區塊內,它會在每次迭代(iteration,§5.4.1,頁183;§6.1.1,頁205)時被創建與摧毀。當fstream物件出了範疇,結束了它的生命週期,與它繫結(關聯,is bound to)的檔案就會自動被關閉。在下一次迭代時,input會再重新創建。
頁319
8.2.2. File Modes
8.2.2檔案模式
表8.4 :檔案模式
第85集 9:54:00
表8.4 :檔案模式
in 開啟檔案來輸入(input)
out 開啟檔案來輸出(output)
app append:每次寫入檔案前都先移到尾端
ate 開啟檔案後、不待寫入,就即刻立刻移到尾端
trunc 截斷(truncate)檔案
binary 以二進位模式(binary mode)進行作業
參見表8.3 : fstream限定的運算
out 模式被設定下才能設定 trunc 模式
只要app 模式被指定,則永遠都會是在 out模式下,即使 out 模式並未被指定
且 app也僅只能在 trunc 模式沒被指定下才能被使用
預設情況下,即使我們沒有指定 trunc 只要是在 out 模式,都是被截斷的(truncated)
只有在以下二個情況下例外(只有在以下兩種情形下,才不會是trunc模式):
1.在 out模式下同時指定 app 模式,這時,我們僅能從檔案的尾端來寫入檔案
第85集 9:38:40 所以要在檔案尾端寫入,就一定要指明「app」模式才可以!
2.在 out 模式下同時指定 in 模式,這時,檔案是開來做輸出和輸入的。 §17.5.3(p.763)會講到對檔案同時做輸出與輸入。10:5:20
ate和 binary 模式則適用於任何檔案資料流型別,而且可以和其他的檔案模式搭配來使用。
改中文版作文:
第85集 9:21:55
Note注意:當一個fstream物件被摧毀,close會自動被呼叫。
習題章節8.2.1
練習8.4 :
撰寫一個函式來開啟一個檔案以進行輸入,並將它的內容讀到一個由string構成的vector中,這個vector中的每個元素分別儲存了該檔案讓每一行的內容都儲存為這個vector中的一個個別元素。
練習8.5 :
改寫前面的程式,將每個字詞都儲存成個別的元素。
練習8.6 :
改寫§ 7.1.1(頁255)的書店程式,從一個檔案讀取其交易記錄。將檔案的名稱作為一個引數傳入給 main (§6.2.5)。
8.2.2開啟檔案的模式(File Modes)
每個檔案資料流都有一個相關聯的檔案模式(file mode)用以代表那個開啟的檔案可以如何被使用利用。表8.4列出了這些檔案模式以及它們的意義。
表8.4
我們只要會在開啟檔案時都可以提供指定一個檔案模式,不論要不是在我們呼叫open的時候,就是或是在我們以用一個 檔案名稱初始化一個資料流而時間接開啟檔案之時。
我們可以指定檔案的模式須遵守有以下列限制規則:
• out只能為ofstream或fstream物件而設定。
• in只能為ifstream或fstream物件設定。
• trunc只能在out也有指定時設定
•只要沒有指定trunc就可以指定app。如果app有指定,檔案永遠都會被開啟在輸出模式,即使out沒有明確指定也是一樣。
•預設情況下,開啟在out模式的檔案都會被截斷(truncated),即使我們沒有指定 trunc也是一樣。要保留以out開啟的檔案之內容,就須指定app,在app指定後,就只能由檔案的尾端寫入;或者是我們同時指定了in模式,這樣檔案就是開啟來讀寫的,就不會被truncated(§ 17.5.3,頁763,會涵蓋討論如何使用同一個檔案來輸入和輸出)。
• ate與binary模式可以在任何的檔案資料流物件型別上指定,並可與任何其他的檔案模式混搭配搭使用。
頁320
改中文版作文:
第85集 10:7:50
每個檔案資料流型別都定義了一種預設的檔案模式,只要我們在開啟檔案時沒有指定模式,這種預設模式就會被套用。在預設模式下,與一個ifstream關聯的檔案會以in模式開啟;與一個ofstream關聯的檔案則會以out開啟;而與一個fstream關聯的檔案則會以in且out的模式開啟。
以out模式開啟的檔案,其既有資料會被抹除
10:13:25
在預設模式下,當我們直接開啟一個ofstream,其關聯的檔案內容就會被抹去。要避免ostream這樣清空檔案內容的唯一方法就只能指定在app模式下開啟 :
// file1在下列這些情況下都會被截斷
ofstream out("file1") ; // out 及 trunc模式是被默許的
ofstream out2("file1",ofstream::out); // trunc 是默認套用上的
ofstream out3("file1", ofstream::out | ofstream::trunc); //「|」應是「或」的意思
//要保留檔案的內容,我們必須明確指定app模式
ofstream app("file2", ofstream::app); // 此時out 是順帶的
ofstream app2("file2", ofstream::out | ofstream::app); //只要「app」有被指名,就可以不用被trunc;app與trunc模式是互斥的
警告:要保留ofstream所開啟檔案的現有資料,唯一的辦法就是要在開啟時明確指定要用app或 in 模式。
open執行時就決定了檔案模式
檔案資料流的檔案模式可在每次開啟檔案時進行更動。
ofstream out; //沒有設定檔案模式
out.open("scratchpad");//未指定,則套用默認的「out且trunc」模式
out.close(); //關閉out,才能開啟別檔
out.open("precious", ofstream::app); //有指定app模式,則此模式會是「out且app」
out.close();
對open的第一次呼叫並沒有明確指定輸出的模式,所以這次檔案就默許以out模式開啟。通常,out就意謂著trunc。(As usual, out implies trunc.)因此,這個與應用程式在同一目錄底下的「scratchpad」檔案,就會被截斷(truncated)。(Therefore, the file named scratchpad in the current directory will be truncated.)而在開啟同一路徑下名為「precious」的檔案時,我們已指明這次的開啟要以附加(append;ofstream::app)模式打開,所以該檔案中的任何資料都能被保留下來,而對它的寫入都只會在檔案的最末端才進行。
注意:不管是明確或隱含地呼叫open,檔案模式都會重新設定。若此時沒有明確指定所需模式,就一概會套用預設模式。
習題章節8.2.2
練習8.7 :
修改前一段練習的的書店程式,將其輸出寫到一個檔案。將該檔案的名稱作為main的第二個引數傳入。
練習8.8 :
10:43:54
修改上一個練習的程式,將其輸出附加到給定的檔案。在同一個輸出檔上執行這個程式至少兩次,以確保資料有被保留。
留言