C++自修入門實境秀、C++ Primer 5版研讀秀 40/ ~v7類別-練習7.2 ~建構器-20190901_182336





decltype(a) c = a;//c is "int";c=a=3;

decltype(a = b) d = a;//d is "int&";d is a ref. of a;d=a=3;

/*because the compiler only analyze the expression of "a=b",

,dose not evaluate the expression ,"a" will still be 3. */

練習2.38

2.6. Defining Our Own Data Structures 資料結構

第40集 1:26:30

在最基本的層次,資料結構是將相關的資料元素聚集成組的一種方式,也是使用那些資料的 一種策略。



In C++ we define our own data types by defining a class.

類別class和資料型別type的關係是這樣來的



程式庫裡頭的型別string、istream、ostream也是經由類別來定義的





類、別:class 類別

型:type 類型、型別



要能自力完整定義一個類別class,須有定義我們自己的運算子operator的能力(見第14章);即使even though再簡單simple的類別也一樣。

2.6.1 定義 Sales_data 型別

一個更為具體的類別

即其資料沒有抽象化。第40集 4:19:00 類別就是包裹,包裹內的都是相關繫的資料:

將同樣的資料元素包裹在一起。

我們使用此類別的策略是,使用者將能夠直接存取這些資料元素,而且必須自行實作所需的運算。

這樣的「資料結構並不支援任何運算」。



struct Sales_data {

std::string bookNo;





In § 7.2 (p. 268 ), we’ll see that C++ has a second keyword, class , that can be used to define our own data structures. We’ll explain in that section why we use struct here.



class scope(範疇)內、外的名稱,是河水不犯井水的關係,所以可以重複使用而不會互相干擾

It is a common mistake among new programmers to forget the semicolon at the end of a class definition.

對於程式設計的新手而言,忘了在class的定義末後加上分號,是很常見的錯誤/疏失。



第40集 1:36:00

Class Data Members 類別的資料成員

新標準才有的類別內初始器:

類別內的初始器(in-class initializer)

我們能使用的類別內初始器的形式(§2.2.1)有所限制:它們必須被包在大括號內,或接著一個=號。我們不可以在括弧內指定一個類別內初始器。

在§7.2中(頁268),我們會看到C++有另一個關鍵字class可用來定義我們自己的資料結構。

第40集 1:44:40 翻成資料結構還不如翻成資料組織(把有關的資料組織起來,統籌運用)。





第40集 1:36:00

Class Data Members 類別的資料成員

新標準才有的類別內初始器:

類別內的初始器(in-class initializer)

我們能使用的類別內初始器的形式(§2.2.1)有所限制:它們必須被包在大括號內,或接著一個=號。我們不可以在括弧內指定一個類別內初始器。

在§7.2中(頁268),我們會看到C++有另一個關鍵字class可用來定義我們自己的資料結構。

第40集 1:44:40 翻成資料結構還不如翻成資料組織(把有關的資料組織起來,統籌運用)。



頁74

練習2.39

struct Foo {/* empty */ }//Note:no semicolon

int main() {

return 0;





我們的Sales_data類別並沒有提供任何運算。Sales_data的使用者必須自行撰寫他們所需的運算。

相加兩個Sales_data物件

我們假設Sales_data類別定義於Sales_data.h中



string 標頭包含了對string的定義,所以要 #include<string>



第40集 2:1:18

頁75

讀取資料到Sales_data物件

Printing the Sum of Two Sales_data Objects

印出兩個Sales_data物件的總和









<< item2.bookNo << '\t' << item2.soldQ << '\t' << item2.revenue << '\n'

<< item3.bookNo << '\t' << item3.soldQ << '\t' << item3.revenue << '\n'

<< item4.bookNo << '\t' << item4.soldQ << '\t' << item4.revenue << '\n' << endl;

}





第40集 28:00

struct Sales_data {

std::string bookNo;

double revenue;

unsigned soldQ;

double bookSize;

};





dataPrevious = Sd;

}

return -1;

}



練習1.22(頁22)

第40集 53:30

struct Sales_data {

std::string bookNo;

double revenue{0.00};

unsigned soldQ{0};

double bookSize;

};





vector<Sales_data> vecSd;

int i = 1;

if (cin>>book.bookNo)//可見每一次>>就移位(shift)了一次,移到沒位,就回傳false

{

while (cin>>book.soldQ>>book.revenue)//這可以處理到讀完輸入止,不限交易記錄筆數

{

vecSd.push_back(book);//可見類別就是把同質性的資料包裹在一起的方式,這裡是book這個「Sales_data」類別就是包裹了至少「bookNo、soldQ、revenue」這三種資料。所以才說「資料結構是將相關的資料元素聚集成組」「將同樣的資料元素包裹在一起。」 第40集 4:17:00

if (!(cin >> book.bookNo))

{

break;

}

}

}





return 0;

}

其標頭檔「Sales_data.h」內容見:練習2.42



§1.6

練習1.25

第40集 1:19:00

練習1.24



2.6.3. Writing Our Own Header Files

撰寫我們自己的標頭檔



當我們在函式外定義一個類別,在任何給定的原始碼檔案中,那個類別的定義只能有一個。

如果我們在數個不同的檔案中使用一個類別,那個類別的定義在各個檔案中都必須相同。

為了確保每個檔案中的類別定義都相同,類別通常定義在標頭檔案(header files)中。







Because a header might be included more than once, we need to write our headers in a way that is safe even if the header is included multiple times.

因為一個標頭不能被引入超過一次,我們必須以引入多次也安全的方式來撰寫我們的標頭。

原文無「不」,翻錯了。翻得與原文不合。

source file

header file

第40集 2:26:20

A Brief Introduction to the Preprocessor

前置處理器的簡介

預處理式的簡介

source 原始碼

要讓一個標頭能被安全地多次引入,最常見的技巧仰賴前置處理器(preprocessor)。前置

頁77





預處理式的簡介

source 原始碼

要讓一個標頭能被安全地多次引入,最常見的技巧仰賴前置處理器(preprocessor)。前置

頁77

原來 #include即是預處理式(前置處理器 preprocessor)處理的對象

一個前置處理器機能,也就是#include。當前置處理器看到一個include,它會將那個#include取代為指定的標頭之內容

C++程式也使用前置處理器來定義標頭守衛(header guards)。標投守衛仰賴前置處理器變數(§2.3.2)。 第40集2:32:00



header guard標頭守衛

要寫自己的標頭檔時要注意前置標頭守衛(header guard)

前置處理器變數(preprocessor variable)



要定義一個預處理式變數要用 #define 這個directive(指示詞)







前置處理器變數的名稱並不遵循C++的範疇規則。

所以其生命週期才不受限制。

前置處理器變數,包括標頭守衛的名稱,在整個程式中都必須是唯一的。



標頭守衛(header guard)習慣上都是以大寫字母寫成以避免與應用程式的其他部分的識別項(indetifier)衝突。

標頭守衛就是前置處理器變數(preprocessor variable)的一種。第40集 2:39:40



Best Practices 養成好習慣

Headers should have guards, even if they aren’t (yet) included by another header. Header guards are trivial to write, and by habitually defining them you don’t need to decide whether they are needed.

即使沒有引入任何其他的標頭,標頭也應該擁有守衛。標頭守衛寫起來很簡單,只要習慣性地定義它們,你就不需要去判斷它們是否必要。

「即使沒有引入任何其他的標頭」據原文應翻作「即使沒有被其他標頭引入」。

只要養成習慣在標頭檔都寫上標頭守衛(header guard),就不用怕在引入#include標頭會有任何引入問題出現





即使沒有引入任何其他的標頭,標頭也應該擁有守衛。標頭守衛寫起來很簡單,只要習慣性地定義它們,你就不需要去判斷它們是否必要。

「即使沒有引入任何其他的標頭」據原文應翻作「即使沒有被其他標頭引入」。

只要養成習慣在標頭檔都寫上標頭守衛(header guard),就不用怕在引入#include標頭會有任何引入問題出現



練習2.42

練習2.41

第40集 3:56:00

#pragma once

#ifndef SALES_DATA_H//此即標頭守衛(header guard)

#define SALES_DATA_H

#include<string>

struct Sales_data {

std::string bookNo;



練習7.2

練習2.41

源碼檔(source file)Sales_data.cpp:

#include "Sales_data.h"

#include<string>

using namespace std;

//這是帶兩個參數的例子

Sales_data& Sales_data::combine(const Sales_data& sales_data1, const Sales_data& sales_data2)

{

Sales_data sd;

sd.bookNo = sales_data1.bookNo;

sd.bookSize = sales_data1.bookSize;

sd.soldQ = sales_data1.soldQ + sales_data2.soldQ;

sd.revenue = sales_data1.revenue + sales_data2.revenue;

return sd; // TODO: insert return statement here

}

/*也可以用 this改寫上式

Sales_data& Sales_data::combine(const Sales_data& sales_data1, const Sales_data& sales_data2)

{

this->bookNo = sales_data1.bookNo;

this->bookSize = sales_data1.bookSize;

this->soldQ = sales_data1.soldQ + sales_data2.soldQ;

this->revenue = sales_data1.revenue + sales_data2.revenue;

return *this; // TODO: insert return statement here

}

*/

//只帶一個參數的例子:

Sales_data& Sales_data::combine(const Sales_data& sales_data)

{

if (bookNo == "") bookNo = sales_data.bookNo;//因為這是Sales_data類別的成員函式,所以可以直接調用該類別內的所有成員名稱,不必全名稱

bookSize = sales_data.bookSize;

soldQ += sales_data.soldQ;

revenue += sales_data.revenue;

return *this;// TODO: insert return statement here

}

string Sales_data::isbn(const Sales_data& sales_data) const

{

return sales_data.bookNo;

}



double Sales_data::avg_price(const Sales_data& sales_data) const

{

if (sales_data.soldQ>0)

{

return sales_data.revenue / sales_data.soldQ;

}

return 0.0;

}


第40集 4:42:00

標頭檔Sales_data.h:

#pragma once

#ifndef SALES_DATA_H

#define SALES_DATA_H

#include<string>

using namespace std;//千萬不要忘了這個

struct Sales_data {

string bookNo;

double revenue{ 0.00 };//總營收-營業額

unsigned soldQ{ 0 };

double bookSize{0.00};

Sales_data& combine(const Sales_data&, const Sales_data&);//成員函式宣告一定要在類別內,定義可在此外

Sales_data& combine(const Sales_data&);//要改變this及其屬性值就不能再在參數列後加上const;因為加上const後this成了指向常值的常值指標,被指向的東西是不能被改變的了

string isbn()const;

double avg_price(const Sales_data&)const;

};

#endif // !SALES_DATA_H

第40集 4:45:30

練習7.3

Revise your transaction-processing program from § 7.1.1 (p. 256) to use these members.用這些成員去修改練習7.1的交易處理程式

中文版又翻錯:

修改§ 7.1.1你寫的交易處理程式,使用那些成員。

源碼檔(source file)編碼很重要,不要用Visual Studio2019預設的Big5碼,這樣GitHub會亂碼,要UTF-8或UTF-8-Bom的才行 6:08:00



#include "Sales_data.h"

using namespace std;



ostream& print(ostream& os, Sales_data sales_data) {

os << sales_data.isbn() << '\t' <<sales_data.soldQ << '\t'

<<sales_data.revenue<< '\t' << sales_data.avg_price();

return os;

}



istream& read(istream& is, Sales_data& sales_data) {//要改變引數值,參數一定要是參考,將引數用傳址(參考)方式傳遞

//decltype(cin)& read(istream is ,Sales_data sales_data) {

is >> sales_data.bookNo>>sales_data.soldQ>>sales_data.revenue;

return is;

}



int main() {

Sales_data total;

if (read(cin, total))

{

Sales_data trans;

while (read(cin, trans))

{

if (total.bookNo == trans.bookNo)

{

total.combine(trans);

}

else {

print(cout, total) << endl;

total = trans;

}

}

print(cout, total) << endl;

}

else

{

cerr << "No data?!" << endl;

}

}



標頭檔Sales_data.h內容為:

#pragma once

#ifndef SALES_DATA_H

#define SALES_DATA_H

#include<string>

using namespace std;//千萬不要忘了這個

struct Sales_data {

string bookNo;

double revenue{ 0.00 };//總營收-營業額

unsigned soldQ{ 0 };

double bookSize{0.00};

Sales_data& combine(const Sales_data&, const Sales_data&);//成員函式宣告一定要在類別內,定義可在此外

Sales_data& combine(const Sales_data&);//要改變this及其屬性值就不能再在參數列後加上const;因為加上const後this成了指向常值的常值指標,被指向的東西是不能被改變的了

string isbn()const;

double avg_price()const;

};

#endif // !SALES_DATA_H





6:24:20

練習7.4

一個類別就是一群相關聯、有關係資料的集合或包裹、群聚……

7:3:30

標頭檔Person.h內容:

#ifndef PERSON_H

#define PERSON_H

#include<string>

using namespace std;

struct Person{

string address;

string name;

};

#endif

7:6:40

練習7.5

學程式真的是欲速則不達啊!

雖然這二、三天花了這麼多時間在此節練習打轉,但真的值得,很多觀念與操作都清楚踏實了。

這題的成員函式一定要const,因為只回傳值,並不打算更改引數。沒有打算更動,就最好右上const來設定唯讀。

參見前「常值成員函式(const member functions)」

用Visual Studio2019內自動創建的檔案要注意編碼,以免到了GitHub成了亂碼

7:40:00

#include <iostream>//標準程式庫才用角括弧

#include "Person.h"

using namespace std;

int main() {

Person p{"士林區","孫守真"};

cout << p.nameMethod() << "在" << p.addressMethod() << endl;

}



源碼檔Person.cpp:

#include "Person.h"



string Person::addressMethod() const

{

return this->address;

}



string Person::nameMethod() const

{

return this->name;

}



標頭檔Person.h:

#ifndef PERSON_H

#define PERSON_H

#include<string>

using namespace std;

struct Person{

string address ;

string name;

string addressMethod()const;

string nameMethod()const;

};

#endif

以上是由VSCodePortable 複製貼上的效果

7:43:20

7.1.3. Defining Nonmember Class-Related Functions

7.1.3定義非成員的類別相關函式

應該就是指「介面」中的函式(參考頁254-255規劃的函式種類),然也:

Although such functions define operations that are conceptually part of the interface of the class, they are not part of the class itself.

輔助函式(auxiliary function)

Functions that are conceptually part of a class, but not defined inside the class, are typically declared (but not defined) in the same header as the class itself.



頁261

非類別成員的函式(即類別介面函式)是與其相關的類別宣告在同一個標頭檔中的。Typically是典型、慣例,卻不是強制、規定的。這樣的設計與安排,是為了讓使用類別者(前面通過使用者可分為類別使用者和應用程式的使用者)只要引入「#include」一個標頭檔就能用到該類別的所有介面機能。



Defining the read and print Functions

定義read和print函式



原來這裡引數與回傳值用參考是因為iostream類別是無法拷貝的:

The IO classes are types that cannot be copied, so we may only pass them by reference (§ 6.2.2, p.210).

不能拷貝就是無法傳值(pass by value),只能傳址(pass by reference)

Moreover, reading or writing to a stream changes that stream, so both functions take ordinary references, not references to const .

何況讀、寫到stream類別,就已經改動它了。會被改動,就不能參考到常值

ordinary references就是前面常講的 plain references,普通的參考

The second thing to note is that print does not print a newline. Ordinarily, functions that do output should do minimal formatting. That way user code can decide whether the newline is needed.

讓類別的使用者vs類別作者自行決定newline要在哪裡出現。

Defining the add Function

Sales_data sum = lhs; // copy data members from lhs into sum

copy就是copy類別裡的資料成員(data member)

頁262

By default, copying a class object copies that object’s members. After

預設情況下,對類別物件的拷貝,就是對其內部成員的拷貝。

8:9:19

練習7.6

練習7.3



練習7.7

Sales_data.h:

#pragma once

#ifndef SALES_DATA_H

#define SALES_DATA_H

#include<string>

using namespace std;//千萬不要忘了這個

struct Sales_data {

string bookNo;

double revenue{ 0.00 };//總營收-營業額

unsigned soldQ{ 0 };

double bookSize{0.00};

Sales_data& combine(const Sales_data&, const Sales_data&);//成員函式宣告一定要在類別內,定義可在此外

Sales_data& combine(const Sales_data&);//要改變this及其屬性值就不能再在參數列後加上const;因為加上const後this成了指向常值的常值指標,被指向的東西是不能被改變的了

string isbn()const;

double avg_price()const;

};

ostream& print(ostream& , const Sales_data&);

istream& read(istream& , Sales_data& );

Sales_data add(const Sales_data&, const Sales_data&);

#endif // !SALES_DATA_H

8:45:00

Sales_data.cpp檔:

ostream& print(ostream& os, const Sales_data& sales_data) {

os << sales_data.isbn() << '\t' << sales_data.soldQ << '\t'

<< sales_data.revenue << '\t' << sales_data.avg_price();

return os;

}



istream& read(istream& is, Sales_data& sales_data) {//要改變引數值,參數一定要是參考,將引數用傳址(參考)方式傳遞

//decltype(cin)& read(istream is ,Sales_data sales_data) {

is >> sales_data.bookNo >> sales_data.soldQ >> sales_data.revenue;

return is;

}



Sales_data add(const Sales_data& sales_data1, const Sales_data& sales_data2)

{

Sales_data sum = sales_data1;

sum.combine(sales_data2);

return sum;

}



練習7.8

因為print函式不必改其引數,唯讀即可;而read函式得把它read進來的寫入它的引數中,所以其參數型別不能是指向常值的參考

8:54:10

練習7.9

9:13:50

#include "Person.h"

#include <iostream>

using namespace std;

int main() {



Person p;

if (read(cin, p))

{

print(cout,p)<<endl;

while (read(cin, p))

print(cout, p) << endl;

}

else

{

cerr << "No data?!" << endl;

}

}



Person.cpp:

#include "Person.h"



string Person::addressMethod() const

{

return address;

}



string Person::nameMethod() const

{

return name;



}

istream& read(istream& is,Person& p){

is>>p.name>>p.address;

return is;

}

ostream& print(ostream& os,const Person& p){

os<<p.nameMethod()<<'\t'<<p.addressMethod();

return os;

}

Person.h

#ifndef PERSON_H

#define PERSON_H

#include<string>

#include<iostream>

using namespace std;

struct Person{

string address ;

string name;

string addressMethod()const;

string nameMethod()const;

};

istream& read(istream&,Person&);

ostream& print(ostream&,const Person&);

#endif



練習7.10

先讀內read(),再讀外邊的

至少要有二筆資料讀取成功,if的區塊才會被執行(if才是true)

9:19:00

7.1.4. Constructors

7.1.4建構器

⑧找對主詞 建構什麼?依類別藍圖建構物件也

C# 也翻成建構子、建構函式

和 C# 一樣,建構子即與類別同名的成員函式

Classes control object initialization by defining one or more special member functions known as constructors .

Constructors have the same name as the class.

建構子沒有回傳型別

建構子的主體也可能是空的述句

建構子非常值成員函式(const member functions)

建構一個常值的類別物件,要等到建構子完成建構了,才能賦予其常值:

When we create a const object of a class type, the object does not assume its “ const ness” until after the constructor completes the object’s initialization. Thus, constructors can write to const objects during their construction.

9:34:40

The Synthesized Default Constructor

合成的預設建構器

預設建構子

頁263

Classes control default initialization by defining a special constructor, known as the default constructor . The default constructor is one that takes no arguments.

預設建構子是不帶任何引數的

預設建構子就好像編譯器自訂的變數_func_ 一樣,都是編譯器會自動為每個函式設定的東西

The compiler-generated constructor is known as the synthesized default constructor .



If there is an in-class initializer (§ 2.6.1, p. 73), use it to initialize the member.

•如果類別中有初始器(initializer,§2.6.1)可用,就用它來初始化該成員。→如果有類別內的初始器,就用它來初始化該成員

前面73頁明明就當作專有名詞類別內的初始器在翻,這裡又不一致

Some Classes Cannot Rely on the Synthesized Default Constructor

某些類別無法仰賴合成的預設建構器

→合成的預設建構器對某些類別並不適用

不能太過依賴(③字形結構換部首 →懶,或②單字想複詞 →賴皮)編譯器為我們預設建構器

一即一切,一切即一:

If we define any constructors, the class will not have a default constructor unless we define that constructor ourselves. The basis for this rule is that if a class requires control to initialize an object in one case, then the class is likely to require control in all cases.

如果我們有定義建構器,類別就不會有預設建 構器,除非我們自行定義那個建構器。

那個建構器是哪個呀?即預設建構器。意指我們也可能自己定義一個預設建構器。

這個規則的基礎是,如果一個類別在某種情況下需要控制物件的初始化,那麼該類別很有可能在所有情況下都需要這種控制。

基礎→基本意涵基本教義、根本意涵 10:4:30 為什麼它要尊重我們自定義的建構器,就是因為它希望有「這種控制」。

10:15:00

具有內建型別或複合型別(compound type)成員的類別,都應該要有自訂的建構器,除非它每個這樣的成員都已經賦予了其相應的類別內的初始器。

頁264

10:18:55

10:25:20

Defining the Sales_data Constructors

預設建構子就是沒有任何參數的:

An empty parameter list (i.e., the default constructor) which as we’ve just seen we must define because we have defined other constructors.

因為已經定義了其他的建構子,就一定也要定義一個預設的建構子。



10:40:00

What = default Means

=default所代表的意義

定義這樣的預設建構器只是為了定義其他的建構器,因為一旦有建構器,就必須也有預設建構器(不能省):

We’ll start by explaining the default constructor:

Sales_data() = default;

First, note that this constructor defines the default constructor because it takes no arguments. We are defining this constructor only because we want to provide other constructors as well as the default constructor. We want this constructor to do exactly the same work as the synthesized version we had been using.

頁265

The = default can appear with the declaration inside the class body or on the definition outside the class body.

Like any other function, if the = default appears inside the class body, the default constructor will be inlined; if it appears on the definition outside the class, the member will not be inlined by default.

預設情況下,在類別內部的函式就是inline(行內函式,inline function),在外部的就不是



10:52:20

Constructor Initializer List

建構器初始器串列(Constructor Initializer List)

Sales_data(const std::string &s): bookNo(s) { }

Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(p*n) { }

在冒號與大括號間的這個部分就是 Constructor Initializer List

初始器的()也可以用{}取代,和一般初始器的寫法是一樣的。

11:11:20

頁266

11:12:40

Defining a Constructor outside the Class Body

在類別主體外定義一個建構器

11:17:00

建構器是沒有回傳型別的

11:20:00

In this constructor there is no constructor initializer list, although technically speaking, it would be more correct to say that the constructor initializer list is empty. Even though the constructor initializer list is empty, the members of this object are still initialized before the constructor body is executed.

這樣的有block(body)的建構器,應該是先作預設初始化,然後才是執行它的block來再次「初始化」(實即asingn指定值給資料成員,data member)

11:31:00

練習7.11

留言

熱門文章