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單元,它擁有兩個名字:x和ref
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.

構造函數和析構函數小結
在創建對象時,類的構造函數負責完成初始化和其它相關操作。
析構函數在對象摧毀時完成相應的清理工作(例如將構造函數分配的資源釋放掉)。
建議為每個帶有數據成員的類設計一個默認構造函數,如果需要,也要設計其他構造函數和析構函數。

類數據成員和類成員函數
類成員:成員屬于類本身,而不屬于類的某個對象。
對象成員或實例成員:屬于對象的成員。前面講過的都是對象成員。
使用關鍵字static可以創建一個類成員。

類數據成員
聲明晶態成員的語法:

class Task {
public:
    static unsigned getN() const (return n;}
    //...
private:
    static unsigned n; // count of Task objects
    //...
};

Task類的數據成員n與Task類本身相關,與任何Task對象無關。
由于n是static的,它對整個Task類而言只有一個,而不是每個Task對象都有一個n。
可以利用n來確定當前存在的Task對象的數量。

Task ( const string & ID) {
    setID( ID);
    logFile = "log.dat";
    setST();
    ft = st;  //no duration ye
    n++; //another Task created
}
~Task() {
    logToFile();
    n--; // another Task destroyed
}

類成員與對象成員實例:

image.png

類數據成員除了必須在類聲明內部用static進行聲明外,還必須在類外進行定義。
定義時可以指定初始值。缺省情況下初始化為0.

class Task {
public:
    //...
private:
   static unsigned n; // count of Task objects
   //...
};
unsinged Task::n = 0; //define static data member

static 數據成員不會影響該類及其對象的sizeof。如下例中表達式sizeof(C)和sizeof(c1)的值都是16。

class C {
    unsigned long dm1;
    double dm2;
    static unsigned long dm3;  //does not impact sizeof (C)
    static double dm4;  //does not impact sizeof(C)
};

類成員函數
除了static數據成員,類還可以有static成員函數。

class Task {
public:
    static unsigned getN() const {return n;}
    //...
private:
    static unsigned n; //count of Task objects
    //...
};

靜態成員函數只能訪問其他的static成員,包括數據成員和成員函數。而非靜態成員函數既可以訪問static成員,也可以訪問非靜態成員。
static成員函數既可以是inline函數,也可以是非inline函數

class Task {
public:
    static unsigned get() {
        setST(); //*****ERROR: NOT static!
        st = time ( 0 ); //***** ERROR: not static!
        return n;   // ok,n is static
    }
    //...
};

訪問static數據成員和static成員函數的方式:
通過對象來訪問;直接通過類來訪問(推薦)

class C{
public :
    static int sVar;
    static void sMeth();
    //...
};
int main(){
    C c1;
    c1.sMeth(); //through an object
    C::sMeth(); //directly and preferred
    unsigned x = c1.sVar; //through an object
    unsigned y = C::sVar; //directly and preferred
    //...
}

在成員函數內定義靜態變量
成員函數內的局部變量可以是static的。如果將成員函數內的某個局部變量定義為靜態變量,該類的所有對象在調用這個成員函數時將共享這個變量。

class C{
public:
    void m() ; //object method
private:
    int x;  //object data member
};
void C::m() {
    static int s = 0; //*****Caution: 1 copy for all objects
    cout << ++s << '\n';
}
int main() {
    C c1,c2;
    c1.m()  //output 1
    c2.m();  //output 2
    c1.m();  //output 3
    return 0;
}

上個例子中,在成員函數m中定義了一個static變量s,由于s定義在程序塊內,它擁有程序塊范圍,因此它只能在m內部訪問。換句話說,該變量只有在函數內部才有效。每調用m一次,s就會相應地增加一次。
因為m是C的成員函數,所以C的所有對象都共享這個靜態局部變量。這樣,不同對象對m的每一次調用訪問的都是同一個s。
相反,對于非靜態局部變量x來說,每個C對象都擁有一個x。

指向對象的指針
對象或對象引用使用成員選擇操作符,來訪問對象的成員。要通過指針來訪問成員,必須使用指針操作符->
在C++中,指向對象的指針主要用于兩個方面:
指向對象的指針可以作為參數傳遞給函數,或通過函數返回。
使用操作符new和new[]動態創建對象,然后返回一個指向該對象的指針。

常量指針this

this是一個關鍵字,this指針只能出現在類的非靜態成員函數中。它指向調用該成員函數的那個對象。靜態成員函數中不能出現this指針。
this指針不是對象的一部分,所以不影響對象的大小。
當一個非靜態成員函數被某對象調用時,編譯器將該對象的地址作為一個隱含的參數傳給該成員函數,例如下面的函數調用:
myDate.setMonth(3);可以看作
setMonth(&myDate,3);
在成員函數內部,可以通過this指針來獲取對象的地址。
大多數情況下,this指針都是隱含使用的。但也可以顯式地使用this指針來調用類的成員。例如:

void Date::setMonth( int mn) {
   month = mn;
   this->month = mn;
   (*this).month = mn;
}

上述三條語句是等價的。
this指針通常用來從成員函數中返回當前對象。
return *this;
this指針有時也用來避免自引用
if (&Object != this)
this指針是一個常量,它不能作為賦值、遞增、遞減等運算的目標對象。此外this只在非static成員函數中用才有效。
與普通函數相比,靜態成員函數由于不與任何對象相聯系,因此它不具有this指針。由于沒有this指針的額外開銷,因此靜態成員函數與類的全局函數相比速度上會有少許的增長。

繼承

C++允許從任何已存在的類派生新類,所派生的類被稱為派生類(derived class),又稱為子類。而已存在的用于派生新類的類被稱為基類(base class),又稱為父類。
在C++中,一個派生類既可以從一個基類派生,也可以從多個基類派生。
從一個基類派生類的繼承被稱為單繼承。
從多個基類派生類的繼承被稱為多繼承。

C++中繼承的語法:
單繼承語法:

class 派生類名 : 訪問控制 基類名
{
    數據成員和成員函數聲明
};

多繼承語法:

class 派生類名 : 訪問控制 基類名1,訪問控制 基類名2,... 訪問控制 基類名n
{
    數據成員和成員函數聲明
};
image.png

類繼承舉例:
單繼承:
class Bear : public ZooAnimal
{ ... };
多繼承:
class Panda : public Bear, public Endagered
{ ... };
其中:
public: 訪問控制關鍵字,指明繼承方式是公有繼承。
當一個類通過公有繼承方式從基類繼承時,基類中的公有成員在派生類中也是公有的。
不指明繼承方式關鍵字public時,編譯器會默認繼承方式為private或protected。
":"用于建立基類與派生類的層次結構。
基類:C++提供的或用戶自定義的類。
派生類中可以定義數據成員和成員函數,此外,還繼承基類所有成員。

class Pen{
public:
    enum int {Off, On};
    void set_status( ink );
    void set_location(int ,int);
private:
    int x;
    int y;
    ink status;
};

class CPen : public Pen {
public:
    void set_color( int );
private:
    int_color;
};
image.png

繼承機制下的私有成員
基類的所有私有成員僅在基類中可見,而在派生類中是不可見的。
但派生類對象會為基類中的所有私有成員分配空間。
如上個例子中,CPen類從Pen類繼承了數據成員x,y和status,盡管這些成員在CPen類中是不可見的,但無論何時創建CPen類的對象時,該對象都將獲得相應的存儲空間來保存x,y和status等數據成員。
盡管在派生類中不能直接訪問基類的私有成員,但可以通過間接的方式(調用從基類繼承來的公有成員函數)進行。
如上個例子中,x,y和status可以通過成員函數set_location和set_status進行訪問。

class Point {
public:
    void set_x( int x1 ) { x = x1; }
    void set_y( int y1 ) { y = y1; }
    int get_x() const {return x;}
    int get_y() const {return y;}
private:
    int x;
    int y;
};
class Intense_point : public Point {
public:
    void set_intensity( int i ) { intensity = i; }
    int get_intensity() const { return intensity;}
private:
    int intensity;
};
image.png

改變訪問限制:
使用using聲明可以改變成員在派生類中的訪問限制。例如:基類中的公有成員一般情況下被繼承為公有成員,但使用using聲明可將其改為私有成員(或保護成員)

class BC { //base class
public:
    void set_x( float a ) { x = a;}
private:
    float x;
};
class DC : public BC { //derived class
public:
    void set_y(float b) { y = b; }
private:
    flaot y;
    usign BC::set_x;
};

這樣就無法直接通過DC類的任何對象調用set_x:

int main() {
    DC d;
    d.set_y(4.31); //OK
    d.set_x(-8.03); // ***** ERROR:set_x is private in DC
    //...
}

如果基類的某個公有成員函數在繼承類中不適合,則可以通過using聲明將其轉變為私有成員函數,從而使它在派生類中隱藏起來。
例如,“有序元素類”從“無序元素類”派生而來,基類“無序元素類”中的某些成員函數可能并不適合“有序元素類”。
如基類中某個成員函數的算法為:在一個無序表中的任意位置插入一個元素。這個算法顯然不符合有序表的處理要求。這樣的成員函數就應通過using聲明將其在派生類中隱藏起來。

名字隱藏
如果派生類添加了一個數據成員,而該成員與基類中的某個數據成員同名,新的數據成員就隱藏了繼承來的同名成員。

保護成員
除了私有和公有成員, C++還提供了保護成員。在沒有繼承的情況下,保護成員和私有成員類似,只
在該類中可見。在公有繼承方式下,保護成員和私有成員具有不同性
質:

  • 基類的保護成員在派生類中是可見的。
  • 而基類的私有成員在派生類中是不可見的。


    image.png
image.png
image.png
image.png
image.png

派生類可對從基類繼承來的保護成員進行訪問,也就是說保護成員在派生類中是可見的。但派生類不能訪問一個基類對象的保護成員,因為基類對象屬于基類,不屬于派生類。
采用public派生
基類的私有成員,在派生類中不可見,基類的私有成員只能被基類的其他成員函數訪問(除friend函數)。
基類的保護成員,在派生類可見,基類的保護成員除了能被基類的其他成員函數訪問外,還能被類層次結構中的所有成員函數訪問。
基類的公有成員,在派生類可見,可被任何函數訪問。

image.png

一般來說,應避免將數據成員設計為保護類型
而是采用私有數據成員與相應保護型訪問函數結合的模式,便于實現數據隱藏(復雜數據成員例外)。

image.png

繼承機制下的構造函數
當創建一個派生類對象時,基類的構造函數被自動調用,用來對派生類對象中的基類部分進行初始化,并完成其他一些相關事務。
如果派生類定義了自己的構造函數,則由該構造函數負責對象中“派生類添加部分”的初始化工作。

image.png
image.png

當基類構造函數的功能對派生類而言不夠用的時候,派生類必須定義自己的構造函數。
可以在派生類的構造函數中調用其基類的構造函數(前提是基類擁有構造函數)

image.png

多米諾骨牌效應:在一個層次很深的類層次結構中,創建一個派生類對象將導致派生鏈中的所有類的構造函數被逐一調用。

image.png
image.png
image.png

如果基類擁有構造函數但沒有默認構造函數,那么派生類的構造函數必須顯式地調用基類的某個構造函數。

image.png
image.png

一般來說,最好為基類提供一個默認構造函數,不但可以避免出現上述問題,而且并不妨礙派生類構造函數去調用基類的非默認構造函數。
假設基類擁有默認構造函數,而其派生類也定義了一些構造函數,不過派生類的任何構造函數都沒有顯式地調用基類的某個構造函數。在這種情況下,當創建一個派生類對象時,基類的默認構造函數將被自動地調用。


image.png

以“DC類從BC 類派生”為例,總結如下:

  • 若DC有構造函數而BC沒有,當創建DC類的對象時,DC的相應構造函數被自動調用。
  • 若DC沒有構造函數而BC有,則BC必須擁有默認構造函數。只有這樣,當創建DC類的對象時,才能自動執行BC的默認構造函數。
  • 若DC有構造函數,且BC有默認構造函數,則創建DC類的對象時,BC的默認構造函數會自動執行,除非當前被調用的派生類構造函數在其初始化段中顯式地調用了BC的非默認構造函數。
  • 若DC和BC都有構造函數,但BC沒有默認構造函數,則DC的每個構造函數必須在其初始化段中顯式地調用BC的某個構造函數。只有這樣,當創建DC的對象時,BC的構造函數才能獲得執行機會。
    在創建派生類對象時,必須顯式地或隱式地執行其基類的某個構造函數,因為有時候,派生類的構造函數可能會依賴基類的構造函數來完成一些必要的操作。例如,依賴基類構造函數來完成部分數據成員的初始化。
image.png

繼承機制下的析構函數
在類的層次結構中,構造函數按基類到派生類的次序執行,析構函數則按派生類到基類的次序執行,因此,析構函數的執行次序和構造函數的執行次序是相反的。

image.png
image.png

由于每個類至多只有一個析構函數,因此對析構函數的調用不會產生二義性,這樣在析構函數中不必顯式地調用其他析構函數,這一點和構造函數的調用規則是不同的。
構造函數回顧

  • 在派生類的對象中,由基類中聲明的數據成員和函數成員所構成的封裝體稱為基類子對象。
  • 基類子對象由基類中聲明的構造函數進行初始化。
  • 構造函數不能被繼承,所以,一個派生類的構造函數必須通過調用基類的某個構造函數來初始化基類子對象。
  • 派生類的構造函數只負責初始化在派生類中聲明的數據成員。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,534評論 1 51
  • 1. 讓自己習慣C++ 條款01:視C++為一個語言聯邦 為了更好的理解C++,我們將C++分解為四個主要次語言:...
    Mr希靈閱讀 2,859評論 0 13
  • 1.面向對象的程序設計思想是什么? 答:把數據結構和對數據結構進行操作的方法封裝形成一個個的對象。 2.什么是類?...
    少帥yangjie閱讀 5,044評論 0 14
  • 一個博客,這個博客記錄了他讀這本書的筆記,總結得不錯。《深度探索C++對象模型》筆記匯總 1. C++對象模型與內...
    Mr希靈閱讀 5,637評論 0 13
  • 1. 結構體和共同體的區別。 定義: 結構體struct:把不同類型的數據組合成一個整體,自定義類型。共同體uni...
    breakfy閱讀 2,138評論 0 22