1. 程式人生 > >C++ 常用語法

C++ 常用語法

C++檔案

例:從檔案income. in中讀入收入直到檔案結束,
並將收入和稅金輸出到檔案tax. out。

#include<iostream>
using namespace std;
const int cutoff = 6000;
const float rate1 = 0.3;
const float rate2 = 0.6;
int main()
{
  ifstream infile;
  ofstream outfile;
  int income,tax;
  infile.open("income.in")
  outfile.open("tax.out")
  while
( infile>>income){ if( income<cutoff) tax = rate1 * income; else tax = rate2 * income; outfile<< "Income = "<< income << "greenbacks\n" << "Tax = " << tax << "greenbacks\n"; } infile.close(); outfile.close(); return
0; }

檢查檔案是否成功開啟

ifstream infile;
infile.open("scores.dat")
if (infile)
  //...
#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
int main()
{
    ifstream infile;
    ofstream outfile;
    infile.open("in.txt");
    outfile.open("out.txt");
    int
num1,num2,num3=0; if(infile && outfile) { while(infile >> num1 >> num2 >> num3){ outfile << setw(2)<< num1<<" "<<num2<<" "<<num3<<" "<<num1+num2+num3<<endl; } } infile.close(); outfile.close(); return 0; }

常量

C++中的const變數能在任何常數可以出現的地方使用,例如陣列的大小、case標號中的表示式。

const int Size = 100;
float a[Size];

bool data type

C++新增bool型別,取值true 或false。用來表示真假。
所有的關係操作符、相等操作符和邏輯操作符現在都
產生bool型別的結果值,而不是int型。
在需要bool型別的地方,整數和指標表示式仍然是允
許的
預設情況下,bool表示式輸出時真值輸出1,假值輸出0.
操作符boolalpha可用來將bool表示式輸出或輸入為false 或true的形式。
操作符noboolalpha可用來將bool表示式輸出或輸入0或1的形式。

bool flag;
flag = (3<5);
cout<<flag<<'\n';
cout<<boolalpha<<flag<<'\n';

1
true

Structure

C++中的結構體和C語言結構體不同。定義結構體變數時可以不加struct關鍵字

struct Point{
  double x,y;
};
Point p1,p2;

C++中的結構體除了包含資料成員,還可以包含函式。

struct Point{
  double x,y;
  void setVal(double,double);
};
p.x = 3.14159;
p.y = 0.0;
p.setVal(4.11,-13.090);

在C++中,類和結構的唯一區別是預設情況下,結構中的所有東西都是Public而類中的所有東西都是Private的.

string 型別

C++提供string型別來替代C語言中以null為結尾的char陣列。
使用string型別必須包含標頭檔案string
有了string型別,程式設計師不再需要關心儲存的分配,也無需處理複雜的null結束字元,這些操作將由系統自動處理。
例項:

#include<string>
using namespace std;
string s1;
string s2="Bravo";
string s3=s2;
string s4(10,'x');

變數s1,已經定義但沒有進行初始化, 預設值為空串
變數s2的初始值是C風格的字串“Bravo”
變數s3用s2初始化,因此s2和s3都代表字串Bravo
變數s4的初始化為10個x。

轉換為C風格的字串:利用函式c_str返回一個指向char型別的陣列的指標

例項:
存放輸入檔名的變數filename的資料型別是string
呼叫ifstream的open函式時,需要一個C風格的字串

string filename = "infile.dat";
ifstream infile;
infile.open( filename.c_str() );

求字串長度,使用函式length

string s = "Ed Wood";
cout << "Length = " << s.length() <<'\n';

輸出為Length = 7.

string的輸入輸出
<<用來輸出string型別的字串

string s1;
string s2 = "Bravo";
string s3 = s2;
string s4(10, 'x');
cout<<s1<<'\n'
       <<s2<<'\n'
       <<s3<<'\n'
       <<s4<<'\n';

輸出為

Bravo
Bravo
xxxxxxxxxx

用來輸入string型別的字串,其預設的動作是忽略空格,然後讀取儲存字元直到檔案結束或遇到另外一個空格。任何空格都不儲存。

string s;
cout << "Enter a string:";
cin >>s;
輸入
Ed Wood

則s的內容為Ed。
注意:在定義後,s實際上長度為0。在讀入字串Ed後,它的長度為2。系統自動提供了充足的儲存空間來儲存這個長度為2的字串。

函式getline:用來讀入一整行到string型別的變數中去。第一個引數是輸入流,第二個引數是string型別的變數。

該函式從輸入流中讀入字元,然後將它們儲存到string變數中,直到出現以下情況為止:
讀入了檔案結束標誌。
都到了一個新行,該新行將從流中移除,但沒有儲存到變數中。
到達字串的最大長度允許值。
如果getline沒有讀入字元,它將返回false,該條件可用於判斷檔案是否結束以終止應用程式

例項:

#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
    string buff;
    ifstream infile;
    ofstream outfile;
    cout<<"Input file name:";
    cin>>buff;
    infile.open(buff.c_str());
    cout<<"Output file name:";
    cin>>buff;
    outfile.open(buff.c_str());
    while(getline(infile,buff))
        outfile<<buff<<"\n\n";
    infile.close();
    outfile.close();
    return 0;
}

輸出資訊的行距是輸入資訊行距的兩倍。

賦值:操作符=可用來進行string型別字串的賦值
操作符左邊必須是一個string型別的字串,右邊可以是一個string字串,也可以是C風格的字串或僅僅是一個char字元。
字串的連線:操作符+和+=可用來進行字串的連線。
操作符+
string + string
string + “xxxxx” 或 “xxxxx” + string
string + ‘x’ 或 ‘x’ + string
而+=,則左邊必須是string字串,右邊可以是一個
string字串、C風格的字串或一個char字元。
–string += string
–string += “xxxx”
–string += ‘x’

函式
應用:通過&來標記,用來為儲存器提供別名

int x;
int &ref = x;
//分配了一個int單元,它擁有兩個名字:xref
x=3;或ref=3;都將3存到int單元

C++預設的呼叫方式和C語言一樣,都是傳值呼叫。如果用&指定一個函式引數為引用引數,則為引用呼叫,引用引數將實際的實參傳給函式,而不是實參的一個拷貝。

#include<iostream>
using namespace std;
void swap(int &,int &);
int main()
{
  int i=7,j=-3;
  swap(i,j);
  cout<<"i="<<i<<'\n'
         <<"j="<<j<<'\n';
  return 0;
}
void swap(int &a,int &b)
{
  int t;
  t = a;
  a = b;
  b = t;
}

函式原型 void swap(int &,int &)指定swap的引數是通過引用傳遞的。
在swap被呼叫後,swap函式體中的a和b直接對應main函式中的i和j的儲存空間。函式swap並不是對i,j的拷貝進行操作,而是直接操作i和j本身。

函式過載:函式名相同,引數個數或引數型別不一樣。
過載函式通常用來對具有相似行為而資料型別不同的操作提供一個通用的名稱。編譯器通過將實參型別與同名函式的引數表進行匹配,以決定應該呼叫哪個函式。

#include<iostream>
#include<iomanip>
using namespace std;
void print(int a);
void print(double a);
int main()
{
  int x = 8;
  double y = 8;
  print(x);
  print(y);
  return 0;
}

void print(int a){
  cout<<a<<'\n';
}
void print(double a)
{
  cout<<showpoint<<a<<'\n';
}

函式簽名:C++要求過載的函式具有不同的簽名。
函式簽名包括:函式名。引數的個數、資料的型別和順序。
為保證函式的唯一性,函式必須擁有獨一無二的簽名。
返回值型別不是函式簽名的一部分,所以函式不能通過返回值型別加以區分。

new和delete操作符

new new[] delete 和 delete[]操作符用於動態分配和釋放儲存空間(例如程式執行的時候)
操作符new分配一個空間;
new[]分配一個數組;
delete釋放由new分配的單一空間;
delete[]釋放由new[]分配的陣列;
與C語言中的函式malloc calloc 和free不同
new、new[] delete delete[]是內建的操作符,而malloc calloc free是庫函式。new和delete是關鍵字

new操作符根據請求分配的型別推斷返回型別和需要分配的位元組數。
給定宣告

int *int_ptr;

通常使用如下方式為int_ptr分配儲存空間;
int_ptr = new int;
如果分配成功,則int_ptr指向所分配的儲存單元。
delete操作符用於釋放由new分配的儲存空間。如果int_ptr指向一個由new分配的單一int單元,則可以這樣釋放它

delete int_ptr;

new[]操作符用於動態分配一個數組

int *int_ptr;
int_ptr = new int[100];//請求分配100個int型別單元

如果分配成功,則int_ptr指向第一個int單元的地址

delete[]操作符用於釋放由new[]分配的儲存空間。如果int_ptr指向一個由new[]分配的int陣列單元,則我們可以這樣釋放它:

delete [] int_ptr;

C++中,一個類就是一種資料型別。
標準C++定義了一些內建類,例如string
通過建立自己的類,程式設計師可以對C++語言進行擴充套件。
通過類宣告可以建立一個類,而且可將這個類當作資料型別來使用。

類和物件
類宣告:描述了封裝在該類中的資料成員和成員函式。

class Human{
       ///...data members and methods go here
};

class是個關鍵字,Human稱為類標籤
通過類宣告建立一個數據型別,類標籤是該資料型別的識別符號或名字。
類宣告中花括號後的分號不可少。
物件定義:從面向物件程式設計角度看,C++中以一個類作為資料型別定義的變數就是物件。
Human maryLeakey;// 如下語句定義了Human的一個物件maryLeakey
物件陣列 Human latvians[365000];
C++的資訊隱藏機制
三個關鍵字:
private:可用來隱藏類的資料成員和成員函式
public:用來暴露類的資料成員和成員函式
protected

面向物件設計的靈魂就是使用private隱藏類的實現,使用public暴露類的介面。
定義一個Person類
介面:包含兩個公有成員函式setAge和getAge
實現:一個unsigned 型別的資料成員age

class Person{
  public:
    void setAge(unsigned n);
    unsigned getAge() const;
  private:
    unsigned age;
};

private成員和public成員可以在類宣告中交叉出現。
Person類的客戶(指Person類的物件的使用者)可通過呼叫公有成員函式setAge和getAge來請求Person類提供服務
Person類的客戶不能訪問屬於類實現部分的私有資料成員age
成員選擇符

class Person{
  public:
    void setAge(unsigned n);
    unsigned getAge() const;
  private:
    unsigned age;
};
int main()
{
  Person boxer;
  boxer.setAge(27);
  //...remainder of main's body

物件的使用者只能訪問類的公有成員(資料成員或成員函式)
類範圍
類的私有成員僅能由類的成員函式訪問,即具有類範圍性質。
類的公有成員擁有公有範圍性質,可以在類之外進行訪問。
在C++中,用關鍵字class宣告的類,其類成員在預設情況下作為私有成員處理,具有類範圍性質。
關鍵字class和struct的區別
使用class關鍵字或struct關鍵字都可以建立類
如果使用class關鍵字,類成員在預設狀態下是私有的。
而是用struct關鍵字,類成員在預設狀態下是公有的。

類成員函式定義

在類宣告之外定義、在類宣告之中進行定義(inline)

class Person{
public:
    void setAge(unsigned n);
    unsigned getAge() const;
private:
    unsigned age;
};

//define Person's setAge
void Person::setAge(unsigned n){
    age = n;
}
//define Person's getAge
unsigned Person::getAge() const{
    return age;
}

在類宣告之外進行定義,為避免重名,在定義成員函式時使用了域解析符::

在類宣告之中進行定義(inline)

class Person{
public:
    void setAge(unsigned n){ age = n;}
    unsigned getAge() const{return age;}
private:
    unsigned age;
};

通過在進行成員函式宣告的時候使用inline關鍵字,可將原本定義在類宣告之外的成員函式強制變成行內函數。

class Person{
public:
    inline void setAge(unsigned n);
    inline unsigned getAge() const;
private:
    unsigned age;
};

//define Person's setAge
void Person::setAge(unsigned n){
    age = n;
}
//define Person's getAge
unsigned Person::getAge() const{
    return age;
}

在程式中使用類
關鍵步驟:類宣告,物件定義,客戶服務請求

#include<iostream>
using namespace std;

class Person{

public:
    void setAge(unsigned n){age = n;}
    unsigned getAge() const{ return age;}

private:
    unsigned age;
};

int main()
{
    Person p;  //create a single person
    Person stooges[3]; //create an array of Persons
    p.setAge(12);
    //set the stooges' age
    stooges[0].setAge(45);
    stooges[1].setAge(46);
    stooges[2].setAge(44);
    //print four ages

    cout<<p.getAge()<<'\n';
    for(int i=0;i<3;i++)
        cout << stooges[i].getAge() << '\n';
    return 0;
}

在程式中使用類
通常將類宣告放到.h中,這樣在使用時通過#include將類宣告包含進來。
如可將Person類的宣告放到person.h檔案中
通常將成員函式的定義放到.cpp中
一般不要將成員函式定義放在.h中,因為標頭檔案通過#include被多個不同的檔案所包含的話可能出現函式重複定義錯

例項程式:堆疊類
問題:建立一個支援int型的壓入和彈出操作的堆疊類。
公有成員:
對stack物件進行初始化。
檢查stack為空,或已滿。
將整數壓入到stack中。
從stack裡彈出整數。
不移出任何元素,將stack的內容輸出到標準輸出。
私有成員:
一個用於列印錯誤資訊的私有成員函式。
三個私有資料成員(top、資料陣列、dummy_val)

#include<iostream>
using namespace std;

class Stack{
public:
    enum{ MaxStack = 5};
    void init(){ top = -1;}
    void push(int n) {
        if(isFull()){
            errMsg("Full stack. Can't push.");
            return;
        }
        arr[++top] = n;
    }
    int pop() {
        if( isEmpty()){
            errMsg("Empty stack. Popping dummy value.");
            return dummy_val;
        }
        return arr[top--];
    }
    bool isEmpty(){ return top<0;}
    bool isFull() { return top>=MaxStack -1;}
    void dump() {
        cout<<"Stack contents, top to bottom:\n";
        for(int i=top;i>=0;i--)
            cout<<'\t'<<arr[i]<<'\n';
    }

private:
    void errMsg(const char* msg) const{
        cerr<< "\n*** Stack operation failure:"<<msg<<'\n';
    }

    int top;
    int arr[MaxStack];
    int dummy_val;
};

int main()
{
    Stack s1;
    s1.init();
    s1.push(9);
    s1.push(4);
    s1.dump(); // 4 9
    cout << "Popping"<<s1.pop()<<'\n';
    s1.dump(); //9
    s1.push(8);
    s1.dump(); // 8 9
    s1.pop();s1.pop();
    s1.dump();//empty
    s1.pop();//still empty
    s1.dump(); //ditto
    s1.push(3);
    s1.push(5);
    s1.dump();//5 3
    //push two too manny to test
    for(unsigned i = 0;i<Stack::MaxStack;i++)
        s1.push(1);
    s1.dump(); //1 1 1 5 3
    return 0;
}

效率和健壯性
- 通過引用來傳遞和返回物件
- const型別引數的物件引用
- const成員函式
- 對成員函式進行過載以便處理兩種型別的字串

通過引用來傳遞和返回物件
物件可以採用傳值方式或引用方式進行物件的傳遞和返回。一般來說應該採用引用方式進行物件的傳遞和返回,而不要採用傳值的方式來進行。因為通過傳值方式來傳遞和返回物件時會降低效率並將面臨物件間的拷貝操作,從而使資料增大,浪費記憶體。
從效率上看,傳遞一個指向物件的指標可收到與引用方式相同的效果,但引用方式的語法要簡練得多。

#include<iostream>
using namespace std;

class C{
public: 
    void set(int n){num = n;}
    int get() const{return num;}

private:
    int num;
};

void f(C&);
C& g();

int main()
{
    C c1,c2;
    f(c1); // pass by reference
    c2 = g(); // return by reference
    cout<< c2.get() <<'\n';
    return 0;
}

void f(C& c){
    c.set(-999);
    cout<<c.get()<<'\n';
}

C& g(){
    static C c3;// NB:static, not auto
    c3.set(123);
    return c3;
}

output:
-999
123 

const型別引數的物件引用
通常,如果一個物件通過引用方式傳到函式f中,而函式f又不會通過修改物件的資料成員的值改變該物件的狀態,那麼,最好將f的引數標記為const,可以預防對引數的誤寫,同時有些編譯器還可對這種情況進行一些優化。
如下例:將函式setName的string型別引數n標記為const,表明setName不會改變n,只是將n賦值給資料成員name。

class C{
public:
    void setName(const string& n) {name = n;}
    // ... other public members
private:
    string name;
};

const成員函式
如果一個成員函式不需要直接或間接(通過呼叫其它的成員函式來改變其物件狀態)地改變該函式所屬物件的任何資料成員,那麼最好將這個成員函式標記為const。
如下例,由於get成員函式不需要改變類C的任何資料成員,因此將get成員函式標記為const。

class C{
public:
    void set(int n) {num = n;}
    int get() const {return num;}
private:
    int num;
};

const 成員函式
定義一個const成員函式時,const關鍵字出現在引數列表與其函式體之間。
由於get成員函式不更改任何資料成員,因此這種型別的函式被稱為只讀函式。將成員函式標記為const可以預防對該函式所屬物件的資料成員的誤寫,同時有些編譯器還可對這種情況進行一些優化。
一個const成員函式僅能呼叫其它const成員函式,因為const成員函式不允許直接或間接地改變物件的狀態,而呼叫非const成員函式可能會間接改變物件的狀態

class C{
public:
    void m1(int x) const{
        m2(x); //*** error: m2 not const
    }
    void m2(int x) { dm = x;}
private:
    int dm;
};

const成員函式
const關鍵字三種不同用法示例:
- 在成員函式set中,因為set不該變string型別引數n,n被標為const。
- 成員函式get返回資料成員name的一個const型引用,此處的const表明誰也不能通過這個引用來修改資料成員name的值。
- 成員函式get本身被標記為const,因為get不會改變類C唯一的資料成員name的值。

class C{
public:
    void set( const string& n) {name = n;}
    const string& get() const{ return name;}
private:
    string name;
};

const返回
- 某函式如果採用const返回,則其返回值只能賦給一個const型別的區域性變數。
- 如果該const返回值是一個類的指標或者引用的話,則不能用該指標或引用呼叫該類的non-const成員函式,因為這些函式可能會改變該類的資料成員的值。

class Foo{
public:
    /*
     * Modifies m_widget and the user may modify the reurned widget.
    */
    Widget *widget();
    /*
     * Does not modify m_widget but the user may modify the returned widget.
     */
    Widget *widget() const;

    /*
    * Modifies m_widget, but the user may not modify the returned widget.
    */
    const Widget *cWidget();

    /*
     * Does not modify m_widget and the user may not modify the returned widget.
    */
    const Widget *cWidget() const;

private:
    Widget *m_widget;
};

int main()
{
    Foo f;
    Widget *w1 = f.widget();  //fine
    Widget *w2 = f.cWidget(); //error -"cWidget()"
                                              // returns a const value
                                              // and "w2" is not const
    const Widget *w3 = f.cWidget(); //fine
    return 0;
}

對成員函式進行過載以便處理兩種型別的字串

class C{
public:
    void set( const string & n) {name = n;}
    void set( const char* n) { name = n;}
    const string& get() const { return name;}
private:
    string name;
};

C c1;
string s1("Who's Afraid of Virginia Woolf?");
c1.set(s1); // string argument

C c2;
c2.set( "What, me worry?"); //const char*

建構函式和解構函式

有些函式比較特殊,在呼叫它們時不需要顯式地提供函式名,編譯器會自動地呼叫它們。
類建構函式(class constructor, 可以有多個)和類解構函式(class destructor,最多一個)就是這種型別的函式,通常編譯器會自動呼叫這兩個函式而不需要我們顯式地發出呼叫動作。

建構函式:是一種與類名相同的成員函式。當建立類的一個例項時(例如,定義一個類的變數時),編譯器會自動地呼叫某個合適的建構函式。下面的例子中,三個成員函式是建構函式,都有著與類相同名稱的Person,而且沒有返回值型別。

class Person{
public:
    Person();// constructor
    Person( const string & n); // constructor
    Person( const char* n); //constructor
    void setName( const string& n);
    void setName( const char* n);
    const string& getName() const;
private:
    string name;
};

建構函式不能有返回型別,因此void Person();是錯誤的。
一個類可以擁有多個建構函式,可以對建構函式進行過載。但每個建構函式必須擁有不同的函式簽名。
上個例子中,三個建構函式具有不同的函式簽名。
第一個沒有引數(預設建構函式),第二個引數型別是const string引用(帶引數溝槽函式),第三個的引數型別是C風格字串const char*(帶引數建構函式)。

建構函式的使用:

#include "Person.h" //class declaration
int main()
{
    Person anonymous; //default constructor
    Person jc("J.Coltrane"); // parameterized constructor
    //...
}

當建立一個物件時,建構函式會被編譯器自動呼叫。程式設計師不需要呼叫建構函式。建構函式主要用來對資料成員進行初始化,並負責其他一些在物件建立時需要處理的事務。建構函式對提高類的健壯性有重要的作用。

之前的stack類沒有建構函式,為保證一個stack正確執行,top成員必須初始化為-1.雖然stack提供了init成員函式來完成這個初始化任務,但程式設計師可能會在建立一個stack物件之後忘了呼叫init成員函式而出錯。可以通過為stack類增加一個預設建構函式,這樣當定義一個stack物件時,編譯器自動呼叫其預設建構函式,預設建構函式再呼叫init成員函式:

class Stack{
    Stack() {init();} //ensures initialization
    //...
};

建構函式最大的特點是:函式名與類名相同,沒有返回型別。
除此之外,建構函式的行為與其他函式相同,也可完成如賦值、條件測試、迴圈、函式呼叫等功能。
建構函式既可以在類宣告之中定義,也可在類宣告之外定義。
下例子中,將預設建構函式定義為inline型別,將帶引數建構函式定義放到類宣告之外。

class Person{
public:
    Person() {name = "Unknown";}
    Person( const string& n);
    Person( const char* n);
    void setName( const string& n);
    void setName( const char* n);
    const string& getName() const;
private:
    string name;
};
Person::Person( const string& n){
    name = n;
}
Person::Person( const char* n){
    name = n;
}

物件陣列與預設建構函式:
如果C是一個類,可以定義任意維數的C物件陣列;
如果C擁有預設建構函式,陣列中每個C物件都會呼叫預設建構函式。

#include<iostream>
using namespace std;
unsigned count = 0;
class C{
public: 
    C() { cout<<"Creating C"<< ++ count<<'\n';}
};
C ar[1000];
本例輸出為
Creating C1
Creating C2
...
Creating C999
Creating C1000

通過建構函式約束物件的建立
C++程式設計師常常會將部分建構函式設計為私有成員,將另一部分設計為公有成員,以確保在建立物件時進行正確的初始化。
一個私有建構函式與普通的私有成員函式一樣,擁有類範圍屬性,因而不能再類之外進行呼叫。
提供私有的預設建構函式

class Emp{
public:
    Emp( unsigned ID) {id = ID;}
    unsigned id; //unique id number
private:
    Emp();//*** declared private for emphasis
    //...
};
int main(){
    Emp elvis;//***** ERROR: Emp() is private
    Emp cher(111222333);// OK, Emp(unsigned) is public
    //...
}

不提供預設建構函式:

class Emp{
public:
    Emp( unsigned ID) {id = ID;}
    unsigned id; //unique id number
private:
    //...
};
Emp elvis; //*** ERROR: no public default constructor

當編譯器在類宣告中找不到任何建構函式時,才會生成一個公有的預設建構函式。如果一個類已經顯式地聲明瞭任何建構函式,編譯器不生成公有的預設建構函式。

拷貝建構函式

拷貝建構函式 建構函式分為兩組:1、預設建構函式,不帶引數 2、帶引數建構函式,需要引數。
在帶引數建構函式中,有兩類很重要的建構函式:
拷貝建構函式:建立一個新的物件,此物件是另外一個物件的拷貝品。
轉型建構函式:用於型別間的轉換,只有一個引數。

拷貝建構函式的原型
必須是引用: Person( const Person&);
Person( Person&);
下面的原型是錯誤的 Person( Person);
拷貝建構函式可以有多於一個的引數,但是第一個以後的所有引數都必須有預設值。例如:
Person( const Person& p, bool married = false);
如果類的設計者不提供拷貝建構函式,編譯器會自動生成一個。它完成如下操作:將源物件所有的資料成員的值注意賦值給目標物件相應的資料成員。
例如:將定類Person沒有定義拷貝建構函式,儘管物件orig和clone擁有不同的儲存空間,但相應的資料成員具有相同的值。

Person orig("Dawn Upshaw");
Person clone(orig);

什麼時候應該為一個類設計一個拷貝建構函式呢?
答:如果一個類包含指向動態儲存空間指標型別的資料成員,則就應為這個類設計拷貝建構函式。

class Namelist {
public:
    Namelist() {size = 0;p=0}
    Namelist( const string [], int);
    void set( const string&, int);
    void set( const char*, int);
    void dump() const;
private:
    int size;
    string* p;
};

Namelist:: Namelist( const string s[], int si) {
    p = new string [size = si];
    for (int i = 0 ;i < size ;i++)
        p[i] = s[i];
}

上例中,沒有為類Namelist定義拷貝建構函式,則下例中定義d2時將會呼叫編譯器提供的拷貝建構函式,將的
的資料成員拷貝到d2

int main()
{
    string list[] = {"Lab","Husky","Collie"};
    Namelist d1(list,3);
    d1.dump();//Lab,Husky,Collie
    Namelist d2(d1);
    d2.dump();//Lab, Husky,Collie
    d2.set("Great Dane",1);
    d2.dump();//Lab, Great Dane, Colli
    d1.dump(); //***** Caution: Lab, Great Dane, Collie
    return 0;
}

這時,指標d1.p和指標d2.p將指向同一塊儲存空間

image.png
潛在危險:操作d1時可能會改變d2的內容,反之亦
然。
為了避免發生潛在錯誤,為Namelist類設計一個滿足要求的拷貝建構函式。

Namelist:: Namelist(const Namelist& d)
{
    p = 0;
    copyIntoP(d);
}
void Namelist::copyIntoP( const Namelist& d) {
    delete [] p;
    if (d.p != 0) {
        p = new string[ size = d.size] ;
        for( int i=0; i<size; i++)
            p[i] = d.p[i];
    }
    else {
        p =  0;
        size = 0;
    }
}

禁止物件拷貝
原因:我們知道,採用傳值方式將物件傳遞給一個函式或者返回一個物件時,將進行物件的拷貝操作。但有些物件很大,比如設計一個Windows類,如果進行物件間拷貝的話,非常費空間和時間。因此需要一種機制,能夠禁止這種情況的發生。
措施:通常採用將拷貝建構函式設計成私有成員的方式,將禁止物件間的拷貝操作。
禁止物件拷貝:

class C{
public:
    C();
private:
    C( C&);
};
void f(C); //*** call by value
C g(); //*** return by value
int main(){
     C c1,c2;
     f( c1); //***** ERROR C(C&) is private!
     c2 = g(); //***** ERROR C(C&) is private!
     //...
}

void f(C cobj) { /*...*/}
C g() {/*...*/}

上個例子中,將類C的拷貝建構函式的宣告放在private區,這樣main函式中對f的呼叫將導致一個嚴重錯誤,因為試圖將c1以傳值方式傳遞給函式f。
要改正這個錯誤,我們就需要修改f,將其引數型別改為類C的引用:void f( C& cobj) {//} // ok, call by reference
main 函式對g的呼叫同樣導致一個嚴重錯誤,因為函式g以傳值方式返回一個C物件,就需要類C擁有一個公有的拷貝建構函式,但類C的拷貝建構函式是私有的。
要避免這個錯誤,必須讓g返回類C的引用:
C& g() {//} //ok,return by reference

轉型建構函式
轉型建構函式是一個單引數的建構函式。它可以將一個物件從一種資料型別(由引數指出)轉換為另一種資料型別(該建構函式所屬的類)

class Person{
public:
    Person() { name = " Unknown"; } //default
    Person( const string& n) {name = n;} //convert
    Person( const char* n) { name = n;} //convert
    //...
private:
    string name;
};

int main()
{
    Person soprano( "Dawn Upshaw");
    //...
}

轉型建構函式可替代函式過載機制,假設函式f的引數型別為Person物件: void f(Person p);// declaration
如果以一個string作為引數來呼叫f:
string s = “Turandot”;
f(s); //string, not Person
只要Person類擁有一個將string轉型為Person的轉型建構函式,那麼編譯器就在string 物件s上呼叫它,以此來構造一個Person物件作為f的引數。
我們稱上例中的Person類的轉型建構函式支援隱式型別轉換,也就是說,該建構函式採用隱藏方式將一個string轉型為一個Person。之所以說它是隱式的,是因為這個轉型動作由編譯器來完成,不需要程式設計人員提供一個明確的轉型操作。
隱式型別轉換提供了方便,但有時會導致一些無法預料到的錯誤,而這些錯誤往往細微得難以察覺。在這種時候,可以關閉這種因轉型建構函式的存在而導致的隱式型別轉換動作,以保證程式的正確性。C++提供的關鍵字explicit可以用來關閉系統的隱式型別轉換功能。

class Person{
public: 
    // convert constructor marked as explicit
    explicit Person( const string& n) {name = n;}
    //...
};

void f( Person s) { /* note: f expects a Person... */}
int main(){
    Person p("foo"); //convert constructor used
    f ( p); //ok p is a Person
    string b = "bar";
    f( b ); //***** ERROR: no implicit type conversion
    return 0;
}

建構函式初始化程式
對const型別的資料成員進行初始化時不能直接賦值,如下列賦值操作是錯誤的。

class C{
public:
    C() {
        x = 0; // ok,x not const
        c = 0;  //***** ERROR: c is const
    }
private:
    int x; //nonconst data member
    const int c; // const data member
};

對const型別的資料成員進行初始化時必須為建構函式新增一個初始化列表

class C{
public:
    C() : c(0) { x = -1;}
private:
    int x;
    const int c;// const data member
};

建構函式的初始化段由一個冒號:開始,緊跟在冒號之後的是需要進行初始化的資料成員,然後是由一對小括號括起來的初始值。
初始化列表僅在建構函式中有效,不能用於其他函式。建構函式的初始化列表可以初始化任何資料成員( const or non const)
但const型別的資料成員只能在初始化列表裡初始化,而不能用其他辦法進行初始化。

class C {
public:
    C() : c( 0 ), x( -1 ) {} //empty body
private:
    int x;
    const int c;//const data member
};

建構函式與操作符 new 和 new[]
當使用動態方式為一個物件分配儲存空間時,C++操作符new和new[]比C函式malloc和calloc做的更好。因為操作符new和new[]在分配儲存空間的同時,還會呼叫相應的建構函式,而malloc和calloc無法完成這個任務。

#include<cstdlib> // for malloc and calloc
class Emp {
public:
    Emp() { /*...*/}
    Emp ( const char* name) {/*...*/}
    //...
};
int main()
{
    Emp* elvis = new Emp();  //default
    Emp* cher = new Emp(" Cher");  //convert
    Emp* losOfEmps = new Emp[1000];  //default
    Emp* foo = malloc ( sizeof ( Emp) ); // no constructor
    //...
    return 0;
}

解構函式
建立類的物件時,會自動呼叫某個合適的建構函式。同樣,當物件被摧毀時,也會自動呼叫一個解構函式。
物件的摧毀出現在如下兩種情況:
以某個類作為資料型別的變數超出其作用範圍。
用delete操作符刪除動態分配的物件。

與建構函式一樣,解構函式也是一個成員函式。
對於類C,其解構函式的原型為: ~C();
由於解構函式不帶引數,因此不能被過載,這樣每個類只能擁有一個解構函式。
與建構函式一樣,解構函式也沒有返回型別,所以void ~C();是錯誤的。

#include<iostream>
#include<string>

using namespace std;
class C{
public:
    C() { //default constructor
        name = "anonynous";
        cout<<name<<" constructing.\n";
    }
    C(const char *n) { //parameterized constructor
        name = n;
        cout<< name<< " constructing.\n";
    }
    ~C() { cout<<name<<" destructing.\n";}
private:
    string name;
};

int main() {
    /* 1 */ C c0("hortense"); //parameterized constructor
    {
        /* 2 */ C c1; //default constructor
        /* 3 */ C c2("foo"); // parameterized constructor
        cout<<'\n';
        /* 4 */ } //c1 and c2 destructors called
        /* 5 */ C *ptr = new C(); //default constructor
        /* 6 */ delete ptr; //destructor for the ptr object
        /* 7 */ return 0; //c0 destructor called
    return 0;
}

console:
hortense constructing.
anonynous constructing.
foo constructing.

foo destructing.
anonynous destructing.
anonynous constructing.
anonynous destructing.
hortense destructing.

建構函式和解構函式小結
在建立物件時,類的建構函式負責完成初始化和其它相關操作。
解構函式在物件摧毀時完成相應的清理工作(例如將建構函式分配的資源釋放掉)。
建議為每個帶有資料成員的類設計一個預設建構函式,如果需要,也要設計其他建構