7.01 利用2.6.1節所定義的Sales_data類為1.6節的交易處理程序編寫一個新的版本。
struct Sales_data
{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
void test701()
{
Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue) {
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) {
if (total.bookNo == trans.bookNo) {
total.units_sold += trans.units_sold;
total.revenue += trans.revenue;
}
else {
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else {
std::cerr << "No data" << endl;
}
}
7.02 為上題中的Sales_data類添加combine和isbn成員。
struct Sales_data
{
std::string bookNo; // 對象的ISBN編號
unsigned units_sold = 0; // 售出的冊數
double revenue = 0.0; // 總價格
std::string isbn () const { return bookNo; } // 返回ISBN編號
Sales_data& combine (const Sales_data&); // 將一個Sales_data對象加到另一個上面
};
Sales_data& Sales_data:: combine (const Sales_data& rhs)
{
this.units_sold += rhs.units_sold;
this.revenue += ths.revenue;
return *this;
}
7.03 修改7.1.1節中的交易程序,令其使用這些成員。
void test703()
{
Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue) {
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) {
if (total.isbn() == trans.isbn()) {
total.combine (trans);
}
else {
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else {
std::cerr << "No data" << endl;
}
}
7.04 編寫一個名為Person的類,使其表示人員的姓名和住址。使用string對象存放這些元素,接下來的練習將不斷充實這個類的其他特征。
7.05 在你的Person類中提供一些操作使其能夠返回姓名和住址。這些函數是否應該是const呢?解釋原因。
要使用const,因為這些函數並不改變它調用的對象的內容。
class Person
{
public:
std::string name;
std::string addr;
std::string getName() const { return name; }
std::string getAddr() const { return addr; }
};
7.06 對於函數add、read和print,定義你自己的版本。
istream& read (istream& is, Sales_data& item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream& print (ostream& os, const Sales_data& item)
{
os << item.isbn() << "\t" << item.units_sold << "\t" << item.revenue << endl;
return os;
}
Sales_data& add(Sales_data& item1, Sales_data& item2)
{
Sales_data sum = item1;
sum.combine(item2);
return sum;
}
7.07 使用這些新函數重寫7.1.2節中的交易處理程序。
void test707()
{
Sales_data total;
if (read(cin, total)) {
Sales_data trans;
while (read(cin, trans)) {
if (total.isbn() == trans.isbn()) {
total.combine (trans);
}
else {
print(cout, total);
total = trans;
}
}
print(cout, total);
}
else {
std::cerr << "No data" << endl;
}
}
7.08 為什么read函數將其Sales_data參數定義成普通的引用,而print函數將其參數定義成常量引用?
因為print函數不會改變對象的值,但是read函數則會改變對象內容。
7.09 對於7.1.2節練習中的代碼,添加讀取和打印Person對象的操作。
std::istream& readPerson(std::istream& is, Person& item)
{
is >> item.name >> item.addr;
return is;
}
std::ostream& printPerson(std::ostream& os, const Person& item)
{
os << "Name : " << item.getName() << "\t Address: " << item.getAddr() << endl;
return os;
}
7.10 在下面這條if語句中,條件部分的作用是什么?
if (read (read(cin, data), data))
一次連續讀入兩個data。read(cin, data)返回的還是cin的引用,在該輸入流上繼續讀入數據。
7.11 在你的Sales_data類中添加構造函數,然后編寫一段程序令其用到每個構造函數。
// 類定義中
{
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double r) : bookNo(s), units_sold(n), revenue(r) {}
Sales_data(std::istream& is);
}
Sales_data::Sales_data (std::istream& is)
{
read(is, *this);
}
// cpp文件中調用
void test711()
{
Sales_data item1;
print(std::cout, item1) << std::endl;
Sales_data item2("0-201-78345-X");
print(std::cout, item2) << std::endl;
Sales_data item3("0-201-78345-X", 3, 20.00);
print(std::cout, item3) << std::endl;
Sales_data item4(std::cin);
print(std::cout, item4) << std::endl;
}
7.12 把只接受一個istream作為參數的構造函數定義到類的內部。
struct Sales_data;
std::istream& read (std::istream& is, Sales_data& item);
struct Sales_data
{
std::string bookNo; // 對象的ISBN編號
unsigned units_sold = 0; // 售出的冊數
double revenue = 0.0; // 總價格
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double r) : bookNo(s), units_sold(n), revenue(r) {}
Sales_data(std::istream& is){ read(is, *this); };
std::string isbn() const { return bookNo; } // 返回ISBN編號
Sales_data& combine (const Sales_data& rhs); // 將一個Sales_data對象加到另一個上面
};
7.13 使用istream構造函數重寫229頁的程序。
void test712()
{
Sales_data total (cin);
if (cin) {
Sales_data trans(cin);
do{ // 要有do while循環,因為Sales_data構造的時候已經讀入一個trans的值。
if (total.isbn() == trans.isbn()) {
total.combine (trans);
}
else {
print(cout, total);
total = trans;
}
} while (read(cin, trans));
print(cout, total);
}
else {
std::cerr << "No data" << endl;
}
}
7.14 編寫一個構造函數,令其用我們提供的類內初始值顯示地初始化成員。
Sales_data () : bookNo(""), units_sold(0), revenue(0) { }
7.15 為你的Person類添加正確的構造函數。
class Person;
std::istream& readPerson(std::istream& is, Person& item);
class Person
{
public:
std::string name;
std::string addr;
std::string const& getName() const { return name; }
std::string const& getAddr() const { return addr; }
Person() = default;
Person(const std::string n, const std::string a) : name(n), addr(a) { }
Person (std::istream& is) { readPerson(is, *this); }
};
void test715()
{
Person p1("lily", "sanqi");
printPerson(cout, p1);
Person p2(cin);
printPerson(cout, p2);
}
7.16 在類的定義中,對於訪問說明符出現的位置和次數有限定嗎?如果有,是什么?什么樣的成員應該定義在public后?什么樣的應該定義在private后?
一個類對訪問說明符出現的次數和位置並沒有嚴格的限定。構造函數和接口函數定義在public之后,而數據成員和部分成員函數定義在private后面。
7.17
class和struct唯一的區別就是默認訪問權限不同,class默認訪問權限為private,而struct則是public。
7.18
封裝就是定義一系列的接口,對用戶隱藏實現細節,用戶在使用時只需要調用接口就可以。
7.19
Person類的構造函數和獲取信息等函數應該設置為public,成員數據設置為private。因為構造函數和獲取信息的函數需要在類外進行調用,而成員數據可以封裝成接口,不需要暴露給用戶。
7.20 友元在什么時候有用?請分別列舉出使用友元的利弊。
友元是類提供給非成員函數訪問類內私有成員的一種機制。優勢是:讓類外函數也可以像類內成員一樣方便的訪問私有成員。缺點是:破壞了類的封裝,寫法較麻煩,必須在類內類外都進行聲明。
7.21 修改Sales_data類使其隱藏實現細節,借助類的新定義重新編譯程序,使其正常工作。
// 對Sales_data類做如下改動
class Sales_data
{
friend std::istream& read (std::istream& is, Sales_data& item);
friend std::ostream& print (std::ostream& os, const Sales_data& item);
friend Sales_data& add(Sales_data* item1, Sales_data& item2);
public:
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double r) : bookNo(s), units_sold(n), revenue(r) {}
Sales_data(std::istream& is){ read(is, *this); };
std::string isbn() const { return bookNo; } // 返回ISBN編號
Sales_data& combine (const Sales_data& rhs); // 將一個Sales_data對象加到另一個上面
private:
std::string bookNo; // 對象的ISBN編號
unsigned units_sold = 0; // 售出的冊數
double revenue = 0.0; // 總價格
};
std::istream& read (std::istream& is, Sales_data& item);
std::ostream& print (std::ostream& os, const Sales_data& item);
Sales_data& add(Sales_data* item1, Sales_data& item2);
7.22 修改你的Person類使其隱藏實現細節。
class Person
{
friend std::istream& readPerson(std::istream& is, Person& item)
public:
Person() = default;
Person(const std::string n, const std::string a) : name(n), addr(a) { }
Person (std::istream& is) { readPerson(is, *this); }
std::string const& getName() const { return name; }
std::string const& getAddr() const { return addr; }
private:
std::string name;
std::string addr;
};
std::istream& readPerson(std::istream& is, Person& item);
std::ostream& printPerson(std::ostream& os, const Person& item);
std::istream& readPerson(std::istream& is, Person& item)
{
is >> item.name >> item.addr;
return is;
}
std::ostream& printPerson(std::ostream& os, const Person& item)
{
os << "Name : " << item.getName() << "\t Address: " << item.getAddr() << endl;
return os;
}
7.23 編寫你自己的Screen類。
class Screen
{
public:
using pos = string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
7.24 給你的Screen類添加三個構造函數:一個默認構造函數;另一個構造函數接受寬和高的值,然后將contents初始化成給定數量的空白;第三個參數接受寬和高的值以及一個字符,該字符作為初始化之后屏幕的內容。
class Screen
{
public:
using pos = string::size_type;
Screen() = default;
Screen(pos wd, pos ht) : width(wh), height(ht), contents(ht*wd, " ") {}
Screen(pos wd, pos ht, char c) : width(wd), height(ht), contents(ht*wd, c) {}
char getChar() const { return contents[cursor]; }
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
7.25 Screen能安全的依賴拷貝和賦值操作的默認版本嗎?如果能,為什么?如果不能,為什么?
可以依賴默認拷貝和賦值操作,因為類中沒有分配內存的操作。
7.26 將Sales_data::avg_price定義成內聯函數。
在Sales_data類中的public下添加下面語句:
inline double avg_price() const { return units_sold ? revenue/units_sold : 0; }
7.27 給你自己的Screen類添加move、set和display函數,通過執行下面的代碼檢驗你的類是否正確。
// screen.h文件
class Screen
{
public:
using pos = std::string::size_type;
Screen() = default;
Screen(pos wd, pos ht) : width(wd), height(ht), contents(ht*wd, ' ') {}
Screen(pos wd, pos ht, char c) : width(wd), height(ht), contents(ht*wd, c) {}
char get() const { return contents[cursor]; }
char get(pos r, pos c) const;
Screen &set(char);
Screen &set(pos, pos, char);
Screen &move(pos r, pos c);
const Screen& display(ostream& os) const;
Screen& display(ostream& os);
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
void do_display(ostream& os) const { os << contents; }
};
inline Screen& Screen::move(pos r, pos c)
{
pos row = r * width;
cursor = row + c;
return *this;
}
inline Screen& Screen::set(char ch)
{
contents[cursor] = ch;
return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch)
{
pos row = r *width;
contents[row+col] = ch;
return *this;
}
Screen& Screen::display(ostream& os)
{
do_display(os);
return *this;
}
const Screen& Screen::display(ostream& os) const
{
do_display(os);
return *this;
}
// cpp文件
void test727()
{
Screen myScreen (5, 5, 'x');
myScreen.move(4, 0).set('#').display(cout);
cout << endl;
myScreen.display(cout);
}
// 打印結果:xxxxxxxxxxxxxxxxxxxx#xxxx
7.28 如果move、set和display函數的返回類型不是Screen&而是Screen,則在上一個練習中會發生什么情況?
如果都返回的是Screen,則move和set都修改的是副本,原始的myScreen並沒有被修改,myScreen.display(cout);語句最終打印出來還是(5, 5, ‘X’)的值。
7.29 修改程序,令move、set和display函數的返回類型是Screen,驗證上題中你的猜測。
// 有&
xxxxxxxxxxxxxxxxxxxx#xxxx
xxxxxxxxxxxxxxxxxxxx#xxxx
// 沒有&
xxxxxxxxxxxxxxxxxxxx#xxxx
xxxxxxxxxxxxxxxxxxxxxxxxx
7.30 通過this指針使用成員的做法雖然合法,但是有點多余。討論顯式地使用指針訪問成員的優缺點。
優點:
1、使程序意圖明確,更易讀;
2、可以使形參名和要賦值的成員名相同。
如:std::string& setName(const string& name) { this->name = name; }
缺點:有些場景下比較多余
std::string const& getName() const { return this->name; }
7.31 定義一對類X和Y,其中X包含一個指向Y的指針,而Y包含一個類型位X的對象。
class X;
class Y;
class X
{
private:
Y* y = nullptr;
};
class Y
{
private:
X x;
};
7.32 定義你自己的Screen和Window_mgr,其中clear是Window_mgr的成員,是Screen的友元。
// 要遵守定義友元函數的依賴關系
class Screen;
class Window_mgr
{
public:
using ScreenIndex = std::vector<Screen>::size_type;
void clear(ScreenIndex);
private:
std::vector<Screen> screens; // 這里不能調用Screen構造函數,否則報錯引用了不完全類型
};
class Screen
{
friend void Window_mgr::clear(ScreenIndex);
/* Screen類的其他定義 */
};
void Window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
7.33 如果我們給Screen添加一個如下所示的size成員將會發生什么情況?如果出來問題,請嘗試修改它。
pos Screen::size() const
{
return height * width;
}
// 將會發生提示pos是不知道的類型。應做如下修改:
Screen::pos Screen::size() const
7.34 如果我們把Screen類中的pos的typedef放在類的最后一行會發生什么情況?
將會提示pos是不知道的類型。
7.35 介紹下面代碼的含義,說明其中的Type和initVal分別使用了哪個定義。如果代碼存在錯誤,嘗試修改它。
typedef string Type;
Type initVal();
class Exercise {
public:
typedef double Type;
Type setVal(Type); // double
Type initVal(); // double
private:
int val;
};
Type Exercise::setVal(Type parm) { // 返回的Type是string類型,形參類型是double類型
val = parm + initVal; // initVal調用的類中的
return val;
}
// 會報錯Type Exercise::setVal(Type parm)匹配不到類中的函數,因為返回值與類中不一致。改為:
// initVal函數只聲明未定義,也會報錯。
Exercise::Type Exercise::setVal(Type parm){}
7.36 下面的初始值是錯誤的,請找出問題並修改。
struct X {
X (int i, int j):base(i), rem(base % j) {}
int rem, base; // 改為int base, rem;
};
// 用一個成員來初始化另一個成員,沒有考慮順序問題
7.37 使用本節提供的Sales_data類,確定初始化下面的變量時分別使用了哪個構造函數,然后羅列出每個對象所有的數據成員的值。
Sales_data first_item(cin); // 使用了 Sales_data(std::istream &is) ; 數據成員值依賴輸入
int main() {
Sales_data next; // 使用了Sales_data(std::string s = ""); bookNo = "", cnt = 0, revenue = 0.0
Sales_data last("9-999-99999-9"); // 使用了 Sales_data(std::string s = ""); bookNo = "9-999-99999-9", cnt = 0, revenue = 0.0
}
7.38 有些情況我們希望提供cin作為接受istream&參數的構造函數的默認實參,請聲明這樣的函數。
Sales_data(std::istream &is = std::cin) { read(is, *this); }
7.39 如果接受string的構造函數和接受istream&的構造函數都使用默認實參,這種行為合法嗎?如果不?為什么?
不合法,如果都使用默認值,不提供實參,則編譯器就不知道該調用哪個構造函數了。
7.40 從下面的抽象概念中選擇一個,思考這樣的類需要哪些數據成員,提供一組合理的構造函數並闡明原因。
class Date
{
public:
Date(int y, int m, int d) : year(y), month(m), day(d) { }
void setYear(int y);
void setMonth(int m);
void setDay(int d);
int getYear();
int getMonth();
int getDay();
private:
int year;
int month;
int day;
};
7.41 使用委托構造函數重新編寫你的Sales_data類,給每個構造函數體添加一條語句,令其一旦執行就打印一條信息。用各種可能的方式分別創建Sales_data對象,認真研究每次輸出的信息直到你確實理解了委托構造函數的執行順序。
void test741()
{
cout << "----------- 1. default: " << endl;
Sales_data s1();
cout << "----------- 2. init bookNo" << endl;
Sales_data s2("999");
cout << "----------- 3.use cin init" << endl;
Sales_data s3(cin);
cout << "----------- 4. use three parameter init: " << endl;
Sales_data s4("s01-999", 2, 59.8);
}
輸出結果如下:
----------- 1. default:
Sales_data(const std::string &s, unsigned n, double r)
default
----------- 2. init bookNo
Sales_data(const std::string &s, unsigned n, double r)
Sales_data(const std::string &s)
----------- 3.use cin init
Sales_data(const std::string &s, unsigned n, double r)
default
iso-99 2 59.2
Sales_data(std::istream& is)
----------- 4. use three parameter init:
Sales_data(const std::string &s, unsigned n, double r)
7.42
7.43 假定有一個類名為NoDefault的類,它有一個接受int的構造函數,但是沒有默認構造函數。定義類c,c有一個NoDefault類型的成員,定義c的默認構造函數。
class NoDefault
{
public:
NoDefault(int v) : value(v){}
private:
int value;
};
class C
{
public:
C() : nd(0){}
private:
NoDefault nd;
};
7.44 下面這條聲明合理嗎?為什么?
vector<NoDefault> vec(10);
非法,因為vector的10個元素沒有被初始化,因此需要默認初始化,而NoDefault類型沒有提供默認構造函數
7.45 如果上題中定義的vector類型是C,則聲明合法嗎?
合法,因為C類提供了默認構造函數。
7.46 下面哪種論斷是不正確的?為什么?
- 一個類必須至少提供一個構造函數。
- 默認構造函數是參數列表為空的構造函數。
- 如果對於類來說不存在有意義的默認值,則類不應該提供默認構造函數。
- 如果類沒有定義默認構造函數,則編譯器將為其生成一個並把每個數據成員初始化成相應類型的默認值。
以上論斷都不正確:
- 類可以不提供構造函數,編譯器會提供一個默認構造函數。
- 默認構造函數為沒有初始化列表(而不是參數列表為空)的對象提供默認初始值,為成員提供默認值的構造函數也稱為默認構造函數。
- 類應該提供默認構造函數。
- 只有當類沒有定義任何構造函數的時候,編譯器才會定義默認構造函數。
7.47 說明接受一個string參數的Sales_data構造函數是否應該是explicit的,並解釋這樣做的優缺點。
Sales_data類的構造函數應該是explicit的。
優點:
- 保證用戶能按照類設計者的初衷進行初始化。
缺點:
- 當只有一個參數時,要進行初始化再使用,沒有隱式轉換的寫法簡潔。
7.48 假定Sales_data的構造函數不是explicit的,下述定義將會執行什么操作?
string null_isbn("9-99-9999-9");
Sales_data item1(null_isbn); // 用string類型的null_isbn直接初始化item1.
string item2("9-99-9999-9"); // 用字符串初始化item2.
7.49 對於combine函數的三種不同聲明,當我們調用i.combine(s)時分別發生什么情況?其中i是一個Sales_data,而s是一個string對象。
- Sales_data &combine(Sales_data); // 正常初始化,將s轉化成Sales_data類型。
- Sales_data &combine(Sales_data&);
// 報錯:error: invalid initialization of non-const reference of type 'Sales_data&' from an rvalue of type 'Sales_data',string不能轉化為Sales_data類型的引用。
- Sales_data &combine(const Sales_data&) const;
// 報錯:error: assignment of member 'Sales_data::units_sold' in read-only object,聲明最后的const會禁止函數對值做出改變。
7.50 確定在你的person類中是否有一些構造函數應該是explicit的。
explicit Person (std::istream& is) { readPerson(is, *this); }
// 只接受一個參數的構造函數應該是explicit
7.51 vector將其單參數的構造函數定義成explicit的,而string則不是,你覺得原因何在?
引用別人對這個問題的闡述如下:
Such as a function like that:
int getSize(const std::vector<int>&);
if vector has not defined its single-argument constructor as explicit. we can use the function like:
getSize(34);
What is this mean? It's very confused.
But the
std::string
is different. In ordinary, we usestd::string
to replaceconst char *
(the C language). so when we call a function like that:void setYourName(std::string); // declaration. setYourName("pezy"); // just fine.
it is very natural.
7.52 使用2.6.1節的Sales_data類,解釋下面的初始化過程。如果存在問題,嘗試修改它。
Sales_data item = {"978-059035", 15, 29.98};
// 要使用這種初始化方式,要求類必須是聚合類。因此Sales_data類需要改成如下形式:
struct Sales_data
{
std::string bookNo;
unsigned int unit_sold;
double revenue;
};
7.53 定義你自己的Debug。
class Debug
{
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) {}
constexpr bool any() { return hw || io || other; }
void set_hw (bool b) { hw = b; }
void set_io (bool b) { io = b; }
void set_other (bool b) { other = b; }
private:
bool hw; // 硬件錯誤
bool io; // io錯誤
bool other; // 其它錯誤
};
7.54 Debug中以set_開頭的成員應該被聲明為constexpr嗎?如果不,為什么?
在c++11中聲明函數是constexpr必須滿足以下條件:
返回值和參數必須是Literal類型
函數體必須只包含一個return語句
函數提可以包含其他的語句,但是這些語句不能在運行期起作用
函數可以不返回常量,但是在調用的時候實參必須傳入常量表達式
因此,如果按照c++11的標准,set_開頭的成員函數不能被聲明為constexpr。
報錯信息:error: assignment of member 'Debug::hw' in read-only object
error: invalid return type 'void' of constexpr function 'constexpr void Debug::set_hw(bool) const'
但c++14好像取消了一些限制,因此c++14編譯不報錯。
7.55 7.5.5節的Data類是字面值常量類嗎?為什么?
數據成員都是字面值類型的聚合類是字面值常量類。 但Data類的數據成員不一定是字面值類型,使用變量或表達式也可以進行初始化。
7.56 什么是類的靜態成員?它有何優點?靜態成員與普通成員有何區別?
類的靜態成員與類本身直接相關,而不是與類的各個對象關聯。
優點:每個對象都不需要單獨存儲靜態成員變量,一旦靜態成員改變了,則每個對象都可以使用新的值。
區別:類的靜態成員屬於類本身,在類加載時就會分配內存,可以通過類名直接進行訪問。
普通成員屬於類的對象,只有在類對象產生時才會分配內存。只能通過對象去訪問。
7.57 編寫你自己的Account類。
class Account
{
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
};
7.58 下面的靜態數據成員的聲明和定義有錯嗎?請解釋原因。
// example.h
class Example
{
public:
static double rate = 6.5; // 錯誤,靜態成員類內初始化應該是一個const表達式。
// 改為 static constexpr double rete = 6.5;
static const int vecSize = 20;
static vector<double> vec(vecSize); // vector不需要在類內就定義大小
// 改為static vector<double> vec;
}
// examplec.cpp
#include "example.h"
double Example::rate;
vector<double> Example::vec;