C++ 中的類和對象及其相關概念和用法

先用一個例子簡單對類和對象有個概念:

// main.cpp
#include <iostream>
#include "myclass.h"

using std::cout;
using std::endl;

int main()
{
    MYSTACK ms(20);
    for(int i = 0;i < ms.length();i++)
        ms.push(i);

    if (ms.isfull())
        cout<<"The stack is full!"<<endl;
    else
        cout<<"The stack is not full!"<<endl;

    ms.display();

    for(int i = 10;i < 15;i++)
        ms.pop();

    if (ms.isempty())
        cout<<"The stack is empty!"<<endl;
    else
        cout<<"The stack is not empty!"<<endl;

    ms.display();

    return 0;
}
// myclass.h
#ifndef MYCLASS_H
#define MYCLASS_H

class MYSTACK
{
public:
    MYSTACK(int s = 100);
    bool isempty();
    bool isfull();
    int length();
    void push(int n);
    int pop();
    void display();
    ~MYSTACK();

private:
    int *head;
    int size;
    int top;
};

#endif // MYCLASS_H
// myclass.cpp
#include <iostream>
#include "myclass.h"

using std::cout;
using std::endl;

MYSTACK::MYSTACK(int s)
{
    size = s;
    head = static_cast<int *>(new int[size]);
    top = 0;
}

bool MYSTACK::isempty()
{
    return top == 0;
}

bool MYSTACK::isfull()
{
    return top == size;
}

int MYSTACK::length()
{
    return size;
}

void MYSTACK::push(int n)
{
    head[top++] = n;
}

int MYSTACK::pop()
{
    return head[--top];
}

void MYSTACK::display()
{
    int i = -1;
    while(i++ != top-1)
        cout<<"The element is "<<head[i]<<endl;
}

MYSTACK::~MYSTACK()
{
    delete []head;
}

結果爲:

The stack is full!
The element is 0
The element is 1
The element is 2
The element is 3
The element is 4
The element is 5
The element is 6
The element is 7
The element is 8
The element is 9
The element is 10
The element is 11
The element is 12
The element is 13
The element is 14
The element is 15
The element is 16
The element is 17
The element is 18
The element is 19
The stack is not empty!
The element is 0
The element is 1
The element is 2
The element is 3
The element is 4
The element is 5
The element is 6
The element is 7
The element is 8
The element is 9
The element is 10
The element is 11
The element is 12
The element is 13
The element is 14

上邊的程序中用到了幾個概念,如類型轉換,new/delete,默認參數,構造器,析構器,多文件編程等。這裏主要對之前未提到的概念做一個總結。

構造器

定義

上邊的例子中,我們構建了一個自定義類 MYSTACK,對應的構造函數就是 MYSTACK(){},因此我們可以得到構造器的定義爲:

class classname
{
    classname(argument)
    {
        statement;
    }
}

作用

構造器的作用就是在類對象創建的時候,完成該對象的初始化,並且該過程是自動完成的,不需要顯式調用。

規則

  • 在對象創建時自動調用,進行初始化
  • 無返回值,與類同名
  • 可以重載,可以有默認參數
  • 如果沒有對構造器進行顯式構建,則系統默認爲該類分配一個構造器
  • 如果對構造器進行顯式構建,則默認構造器消失,以自實現爲準

比如我們之前構建的類,就子實現了構造器,並採用了默認參數。

需要注意的是,如果函數聲明和函數定義分開書寫的話,只在聲明處說明即可。

參數初始化列表

一般情況下,構造器中主要是對類對象的數據成員進行初始化。此時數據成員的初始化既可以在構造器的函數體中實現,也可以以參數初始化列表的形式實現。參數初始化列表的形式爲:

classname(argument list):data1(init value),data1(init value),...
{
    statement;
}

參數初始化列表中以 : 爲分界線,之前的部分爲函數頭,之後的部分爲參數初始化列表,參數之間用逗號 ,分隔,init value 可以是表達式或者是 argument list 中的值,也可以是常量。比如我們可以將之前的構造器改爲:

MYSTACK::MYSTACK(int s):head(static_cast<int *>(new int[s])),size(s),top(0){}

這樣也是正確的,但是下邊的修改可能結果不會報錯,但是卻是有問題的:

​MYSTACK::MYSTACK(int s):size(s),head(static_cast<int *>(new int[size])),top(0){}

上邊的結果會提示:

warning: 'MYSTACK::size' will be initialized after [-Wreorder]

 好像是提示初始化的順序。再看一個關於參數初始化列表的例子:

#include <iostream>
#include <string>

using namespace std;

class A
{
public:
    A(string ps):name(ps),len(name.size()){}

    void dis()
    {
        cout<<len<<endl;
        cout<<name<<endl;
        cout<<name.size()<<endl;
    }
private:
    int len;
    string name;
};

int main()
{
    A a("Apple");
    a.dis();
    return 0;
}

 結果爲:

94
Apple
5

上邊的結果卻跟我們預想的不太一樣,並且出現了和之前相同的警告,這說明:

  • 參數初始化列表只能用於構造器中
  • 初始化列表中的初始化順序,至於數據成員的聲明順序有關
  • 必須使用此格式來初始化非靜態 const 數據成員(c++98)
  • 必須使用此格式來初始化引用數據

析構器

定義

上邊的例子中,我們構建了一個自定義類 MYSTACK,對應的析構函數就是 ~MYSTACK(){},因此我們可以得到析構器的定義爲:

class classname
{
    ~classname()
    {
        statement;
    }
}

作用

析構器的作用就是在類對象銷燬的時候,完成類對象的銷燬,尤其是類中申請了堆上內存。

類對象銷燬時期

  • 棧對象離開其作用域
  • 堆對象被手動delete

規則

  • 在對象銷燬時自動調用,進行類對象的銷燬
  • 無返回值,與類同名
  • 沒有參數
  • 不可以重載
  • 如果沒有對析構器進行顯式構建,則系統默認爲該類分配一個析構器
  • 如果對析構器進行顯式構建,則默認析構器消失,以自實現爲準

這裏將之前的析構函數修改爲下邊的形式,簡單說明棧對象和堆對象的析構器調用:

MYSTACK::~MYSTACK()
{
    cout<<"~MYSTACK()"<<endl;
    delete []head;
}
// main.cpp
#include <iostream>
#include "myclass.h"

using std::cout;
using std::endl;

int main()
{
    MYSTACK ms;

    MYSTACK *p = new MYSTACK;
    delete p;

    return 0;
}

結果爲:

~MYSTACK()
~MYSTACK()

上邊的結果顯示,堆對象在手動 delete 的時候調用了析構器,棧對象在 main 結束的時候調用了析構器。

拷貝構造

定義

class classname
{
    classname(const classname & var)
    {
        statement;
    }
}

作用

由已經存在的對象,創建新的對象。也就是說該新對象是拷貝舊對象得到的。

拷貝構造發生時期

  • 類對象拷貝,製作副本
  • 以類對象作爲參數和返回值

規則

  • 拷貝構造默認爲等位拷貝,也就是淺拷貝
  • 深拷貝需要自實現
  • 如果沒有對拷貝構造進行顯式構建,則系統默認爲該類分配一個拷貝構造
  • 如果對拷貝構造進行顯式構建,則默認拷貝構造消失,以自實現爲準

深拷貝和淺拷貝

深拷貝與淺拷貝的不同主要發生在存在堆上空間的時候,兩者之間的關係可以用下圖說明:

從上圖中我們可以看出,深拷貝和淺拷貝的不同主要分爲兩種情況:

  • 如果類對象的數據成員沒有申請堆上空間的話,深拷貝和淺拷貝是一樣的
  • 如果類對象的數據成員申請了堆上空間的話,深拷貝會將源對象的堆上空間也進行拷貝,而淺拷貝只是做了一個鏈接
#include <iostream>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):name(name),num(num),grade(grade){}

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT(){}

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

結果爲:

name is zhangsan
num is 0001
grade is 80
name is zhangsan
num is 0001
grade is 80

從上邊的結果可以看出兩者的結果是一樣的,但是如果選擇爲 name 成員申請堆上空間就會出現不同的情況:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

使用 g++ 編譯運行結果爲:

name is zhangsan
num is 100
grade is 80
name is zhangsan
num is 100
grade is 80
*** Error in `./a.out': double free or corruption (fasttop): 0x0000000000c5ec20 ***
...

也就是說在 main 函數中,對於 st 和 sd 的 name 成員指向的堆上空間進行了多次釋放。這也就是淺拷貝的不足,可以另外構建深拷貝的拷貝構造函數來避免該問題:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

結果爲:

name is zhangsan
num is 100
grade is 80
name is zhangsan
num is 100
grade is 80

但是在修改的時候,發現如果將程序改爲:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, char *num, int grade):grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        this->num = new char[strlen(num)+1];
        strcpy(this->num,num);
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
        delete []num;
    }

private:
    char *name;
    char *num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan","100",80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

上邊的程序中同樣發生了 double free,但是在 Linux 中編譯運行的結果卻沒有顯示錯誤。現在還不知道什麼原因。

特性

在發生淺拷貝時,如果源對象數據成員申請的有堆上空間,那麼源對象和拷貝對象的該數據都指向同一塊堆上空間。

傳參

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    ~STUDENT()
    {
        cout<<"~STUDENT"<<endl;
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

void func(STUDENT obj){}

int main()
{
    STUDENT st("zhangsan",100,80);

    func(st);

    return 0;
}

結果爲:

~STUDENT
~STUDENT

上邊的程序中我們只定義了一個變量,但是卻調用了兩次析構函數,原因就在於調用函數 func 時發生了拷貝構造。修改爲:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    ~STUDENT()
    {
        cout<<"~STUDENT"<<endl;
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

void func(STUDENT &obj){}

int main()
{
    STUDENT st("zhangsan",100,80);

    func(st);

    return 0;
}

就只會發生一次調用,原因在於函數中的參數形式是一個引用,而引用只是一個變量別名,擴展了變量的作用域而已,並不發生拷貝構造。

this指針

觀察上一個程序的構造函數:

STUDENT(char *name, int num, int grade):num(num),grade(grade)
{
    this->name = new char[strlen(name)+1];
    strcpy(this->name,name);
}

在上邊的函數中,函數形參是 name,但是數據成員中也有 name,但我們使用了 this 指針來區分兩者。

作用

  • 類對象在被創建時,會自動生成指向當前對象的指針
  • 避免函數形參與數據成員同名
  • 基於 this 指針的自身引用也廣泛使用於多重串聯調用的函數中

多重串聯調用的用法有點類似於流對象的用法:

#include <iostream>
#include <string.h>

using namespace std;

class A
{
public:
    A(int data = 0):data(data){}

    A &display()
    {
        this->data++;
        cout<<data<<endl;
        return *this;
    }

    ~A(){}

private:
    int data;
};

int main()
{
    A obj;

    obj.display().display().display();

    return 0;
}

結果爲:

1
2
3

上邊的程序藉由 this 指針實現了函數的多重串聯調用。

= 重載

定義

class classname
{
    A & operator=(const classname &obj)
    {
        statement;
        return *this;
    }
}

作用

對運算符 = 進行重載可以實現類對象之間的賦值。

規則

  • = 重載默認爲等位拷貝,也就是淺賦值
  • = 重載的深賦值需要自實現
  • 返回引用,且不必使用 const 修飾
  • 如果沒有對 = 重載進行顯式構建,則系統默認爲該類分配一個 = 重載
  • 如果對 = 重載進行顯式構建,則默認 = 重載消失,以自實現爲準

淺賦值和深賦值

這一點跟拷貝構造要說明的問題一樣,只是拷貝是從無到有,而賦值重載則是覆蓋重寫。但是深賦值還需要考慮其它問題。

內存泄漏

拷貝構造發生在對象創建時,將源對象拷貝到拷貝對象當中。如果源對象存在堆上空間,只需要對應的申請同等大小的堆上空間,然後再將源對象的堆上內容複製一份即可。

而 = 重載的前提是先構建了兩個對象,然後將源對象賦值到賦值對象。因此源對象和賦值對象對應的堆上空間可能是不同的:

在上圖中,如果賦值對象直接申請同等大小的堆上空間,則會造成原來申請的堆上空間的泄露。因此需要先釋放原來的堆上空間,再申請空間,再進行復制。

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    STUDENT sd("lisi",101,90);

    st.display();
    sd.display();

    sd = st;

    st.display();
    sd.display();

    return 0;
}

 結果爲:

name is zhangsan
num is 100
grade is 80
name is lisi
num is 101
grade is 90
name is zhangsan
num is 100
grade is 80
name is zhangsan
num is 100
grade is 80

上邊的 = 重載只是在拷貝構造的基礎之上,釋放了賦值對象的 name 空間,然後返回了 *this。

自賦值

但是上邊的程序卻無法實現 st = st 的操作,因爲會將原來 name 指向的空間給釋放掉,因此對於上邊的 = 重載函數,可以修改爲:

STUDENT & operator =(const STUDENT &obj)
{
    if(this == &obj)
        return *this;

    delete [](this->name);

    this->name = new char[strlen(obj.name)+1];
    strcpy(this->name,obj.name);
    this->num = obj.num;
    this->grade = obj.grade;

    return *this;
}

這樣就能夠解決自賦值的問題。

A=B=C/(A=B)=C

通常 = 重載前不加 const,所造成的差別主要在 A=B=C/(A=B)=C 能夠實現。

  • 如果 = 重載前沒有 const 則可以實現 A=B=C/(A=B)=C,即等式串聯和表達式賦值
  • 如果 = 重載前有 const 則只能實現 A=B=C,原因在於 = 重載的返回值爲 const,是不可修改的

返回棧上引用和對象

C++ 返回棧變量

#include <iostream>

using std::cout;
using std::endl;

int func()
{
    int a = 1;
    return a;
}

int main()
{
    cout<<func()<<endl;

    return 0;
}

函數 func 在發生調用時,先在棧上產生了變量 a=1,然後將該變量返回。正常情況下,函數調用結束時該函數在棧上的變量會被釋放。但同時返回值會被暫時存儲在寄存器上,然後函數返回,完成賦值。

C++ 返回棧對象

測試類爲:

class TEST
{
public:
    TEST()
    {
        cout<<"TEST()"<<endl;
    }

    TEST(const TEST & obj)
    {
        cout<<"TEST(const TEST & obj)"<<endl;
    }

    TEST & operator=(const TEST & obj)
    {
        cout<<"TEST & operator=(const TEST & obj)"<<endl;
        return *this;
    }

    ~TEST()
    {
        cout<<"~TEST()"<<endl;
    }
};

傳值

void func(TEST var){}

int main()
{
    TEST t;

    func(t);

    return 0;
}

 結果爲:

TEST()
TEST(const TEST & obj)
~TEST()
~TEST()

該部分內容之前提到過,在傳值時發生了拷貝構造,因此就是一次構造,一次拷貝構造,一次析構。

傳引用

void func(TEST &var){}

int main()
{
    TEST t;

    func(t);

    return 0;
}

結果爲:

TEST()
~TEST()

該部分內容之前提到過,引用只是變量別名,擴展了作用域,因此發生了一次構造,一次析構。

返回對象(無人接)

TEST func(TEST &var){return var;}

int main()
{
    TEST t;

    func(t);

    return 0;
}

結果爲:

TEST()
TEST(const TEST & obj)
~TEST()
~TEST()

返回值先被拷貝到臨時空間中,因此發生了一次構造,一次拷貝構造,二次析構。

返回對象(新對象接)

TEST func(TEST &var)
{
    cout<<static_cast<void *>(&var)<<endl;
    return var;
}

int main()
{
    TEST t;

    TEST tt = func(t);
    cout<<static_cast<void *>(&tt)<<endl;

    return 0;
}

結果爲:

TEST()
0x61fe9f
TEST(const TEST & obj)
0x61fe9e
~TEST()
~TEST()

和之前的結果一樣,只是臨時空間變成了對象 tt 的空間,也是發生了一次構造,一次拷貝構造,二次析構。

返回對象(已存在對象接)

TEST func(TEST &var)
{
    cout<<static_cast<void *>(&var)<<endl;
    return var;
}

int main()
{
    TEST t;

    TEST tt;
    tt = func(t);
    cout<<static_cast<void *>(&tt)<<endl;

    return 0;
}

結果爲:

TEST()
TEST()
0x61fe9e
TEST(const TEST & obj)
TEST & operator=(const TEST & obj)
~TEST()
0x61fe9d
~TEST()
~TEST()

此時先構造 t 和 tt,然後函數 func 返回值通過拷貝構造產生了中間變量,中間變量再向 tt 賦值。因此發生了 兩次構造,一次拷貝構造,一次賦值,三次析構。

無形參返回引用

TEST &func()
{
    TEST var;
    return var;
}

int main()
{
    TEST t = func();

    return 0;
}

結果爲:

TEST()
~TEST()
TEST(const TEST & obj)
~TEST()

首先是函數 func 中 var 的構造,析構。然後返回值的臨時變量拷貝到 t,最後析構 t。因此發生了一次構造,一次拷貝構造,兩次析構。

因爲返回的是已經析構了的對象,不知道會發生什麼,因此不能這麼使用。

有形參返回引用

比如之前實現的 = 重載的形式,能夠實現函數的多重串聯調用。

棧和堆上的對象及對象數組

類對象的定義和初始化

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT sarr[3] = {STUDENT("zhangsan",100,60),
                       STUDENT("lisi",101,70),STUDENT("wangwu",101,80)};

    STUDENT *p = new STUDENT[3]{STUDENT("zhangsan",100,60),
                       STUDENT("lisi",101,70),STUDENT("wangwu",102,90)};     // C11

    for(int i=0;i<3;i++)
        sarr[i].display();

    for(int i=0;i<3;i++)
        p[i].display();

    return 0;
}

結果爲:

name is zhangsan
num is 100
grade is 60
name is lisi
num is 101
grade is 70
name is wangwu
num is 101
grade is 80
name is zhangsan
num is 100
grade is 60
name is lisi
num is 101
grade is 70
name is wangwu
num is 102
grade is 90

注意事項

  • 使用 new 創建一個類對象會調用對應的構造函數,相當於定義一個類對象。
  • new 構建的類對象需要使用 delete 釋放,釋放時默認調用析構函數
  • 不論在棧上還是堆上生成的數組,都會調用構造函數,調用次數爲數組元素個數
  • 類對象數組初始化形式比較固定,不能部分初始化,只能全部初始化。

類對象的存儲方式

之前在 C 語言中曾經提到過,函數名也是一個指針,也是存儲在內存當中的。那麼對於類來說是否需要存儲每個對象的數據成員和成員函數呢?這裏我們借用之前提到的類 STUDENT 來說明這個問題:

int main()
{
    STUDENT sd("zhangsan",100,60);

    STUDENT st("lisi",101,70);

    cout<<sizeof(sd)<<endl;
    cout<<sizeof(st)<<endl;
    cout<<sizeof(STUDENT)<<endl;

    return 0;
}

結果爲:

12
12
12

上面的結果表明,該類對象的大小是恆定的,對應的空間剛好是單個對象的數據成員所佔內存的大小,而不包括函數代碼所佔用的空間。

原理

類對象只存儲對應的數據成員,而函數方法卻是調用公用的函數代碼段。又因爲不用的對象所使用的數據成員是不同的,因此就需要一種方法去實現這種機制。這時候就用到了 this 指針。

之前提到過 this 指針指向調用該函數的不同對象,因此就能夠保證能夠對應找到對應的數據成員,同時又大大節省了內存空間。

成員函數無論是在類內定義還是在類外定義,成員函數代碼的存儲方式都相同。如果多少了解一點 python 語言的話,會發現裏邊的類成員函數第一個參數一般是 self。而 C++ 中的 this 指針就可以認爲是這個 self。至於說哪個在前,哪個在後,則可能是 python 只是顯式給出了這種原理,而 C++ 內部則將之封裝了起來。

const

const 數據成員

我們在 C 語言中說過,const 修飾的成員爲常量,是不能夠修改的,一般需要在定義的時候完成初始化。而 const 修飾的類數據成員的性質爲:

  • const 修飾的類數據成員,表示該成員爲常量,不能夠被修改
  • const 數據成員只能通過參數初始化列表進行初始化
  • 能夠被 const 和非 const 成員函數調用,而不能進行修改

此時的類變爲:

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade,int school):num(num),grade(grade),school(school)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj):school(obj.school)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
        cout<<"school is "<<school<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    const int school;
};

需要注意的是,在構造函數和拷貝構造函數中都應該至少給出 const 數據成員的參數初始化列表。

const 成員函數

定義

type funcname(argument) const
{
    statement;
}

含義

  • const 修飾成員函數表示在本函數內部不會修改類內的數據成員
  • 也不會調用其它非 const 成員函數
  • const 修飾的類成員函數聲明和定義如果分開書寫的話,const 都不能省略
  • const 還能加載函數頭的開頭,此時的 const 是修飾的返回值,而不是函數本身

const 構成函數重載

我們之前說過函數重載的規則爲:

  • 函數名相同
  • 參數個數不同,參數類型不同,參數順序不同,都能夠構成函數重載
  • 返回值類型相同

而 const 用來修飾函數的話,也符合函數重載的概念,因此也能夠構成重載:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display()
    {
        name = "*******";
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);
    const STUDENT sd("lisi",101,90);

    st.display();
    sd.display();

    return 0;
}

結果爲:

name is *******
num is 100
grade is 80
name is lisi
num is 101
grade is 90

從上邊的結果可知:

  • 如果 const 構成函數重載,const 對象只能調用 const,非 const 對象優先調用非 const 函數
  • const 函數只能調用 const 函數,非 const 函數可以調用非 const 函數
  • 如果會使用到 const 對象,最好使用成員函數的 const 版本,或者將之重載
  • const 修飾的類成員函數聲明和定義如果分開書寫的話,const 都不能省略
  • const 還能加載函數頭的開頭,此時的 const 是修飾的返回值,而不是函數本身

const 對象

定義

const classname var;

規則

  • const 對象只能調用 const 成員函數
  • 可以訪問 const 成員或者非 const 成員,但是不能修改

static

static 數據成員

之前在 C 中使用到 static 時,我們提到:

static 修飾局部變量時:

  • 會更改局部變量的生命週期,使其與進程一致
  • static 修飾的局部變量如果未初始化,會被初始化爲 0

static 修飾全局變量時:

  • 會限制該變量的外延性,使其成爲只能在本文件內部使用的全局變量
  • 避免了命名污染

但是在 C++ 中,靜態成員表示該成員屬於整個類,而不是某個對象,所有的對象共享定義的靜態成員。因此使用靜態成員變量可以實現數據在多個對象之間而不是全局範圍內的共享。

不過需要申明的一點是雖然可以通過類對象調用靜態成員,但是靜態成員實際上是屬於類的。

聲明

static datatype static_var;             // 類內定義

初始化

datatype classname::static_var = init_value;             // 類外初始化

在類外進行初始化時,不需要 static 關鍵字。

調用

之前提到過,靜態成員可以通過類對象實現調用,但是靜態成員本質上還是屬於類的,因此可以通過兩種方法實現調用:

classname::static_var
class_obj.static_var

實例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member+this->name+" ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
        cout<<"member is "<<STUDENT::member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);
    st.display();
    STUDENT sd("lisi",101,90);
    sd.display();

    cout<<sizeof(STUDENT)<<endl;

    return 0;
}

結果爲:

name is zhangsan
num is 100
grade is 80
member is zhangsan
name is lisi
num is 101
grade is 90
member is zhangsan lisi
12

在上邊的例子中,借用靜態成員實現了成員的統計,可以看出:

  • static 成員能夠實現類間對象的信息共享
  • static 成員不佔用類空間,在類外存儲
  • static 成員從形式上看,是屬於類的全局變量,存儲在 data 區的 rw 段
  • static 和 const 成員一樣必須要進行初始化,不同的是,static 成員需要在類外初始化
  • 可以通過命名空間即 classname::static_var 訪問,也可以通過對象訪問的形式 class_obj.static_var 訪問

static 成員函數

聲明

static datatype funcname(argument)
{
    statements;
}

作用

用來管理靜態成員,靜態函數只能夠訪問靜態成員。

調用方式

同靜態成員類似,也可以通過兩種方式進行調用:

classname::static_func();
class_obj.static_func();

實例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member+this->name+" ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<STUDENT::member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);
    st.display();
    STUDENT::dismem();

    STUDENT sd("lisi",101,90);
    sd.display();
    STUDENT::dismem();

    return 0;
}

結果爲:

name is zhangsan
num is 100
grade is 80
member is zhangsan
name is lisi
num is 101
grade is 90
member is zhangsan lisi

上邊的結果跟之前的結果一樣,從上邊的結果也可以看出:

  • 靜態成員函數的意義只是就爲了管理靜態數據成員,完成對靜態數據成員的訪問,能夠更好的完成對靜態數據成員的封裝
  • 靜態成員函數只能訪問靜態數據成員。可以想想如果使用 classname::static_func() 的形式訪問,就會無法確定 this 指針指向的變量
  • 靜態成員函數還不能夠被 const 修飾。因爲 const 成員函數用來修飾該成員函數的隱式 this 指針爲 const,而 static 成員函數中不存在 this 指針 

static const

const 數據成員不可更改,static 數據成員可以實現類內共享。如果一個類數據成員既不能夠被改變,也要在類內共享,就需要使用 static const 修飾。

  • 修飾數據成員,必須要在類內初始化,static const 合寫在函數頭的最前邊,static const 誰先誰後無所謂
  • 修飾成員函數,static const 合寫在函數頭的最前邊,static const 誰先誰後無所謂
#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    const static void dismem()
    {
        cout<<"member number is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    static const int member = 100;
};

int main()
{
    STUDENT st("zhangsan",100,80);
    st.display();
    st.dismem();

    return 0;
}

結果爲:

name is zhangsan
num is 100
grade is 80
member number is 100

注意事項 

在測試上邊實例的時候,發現只能使用 integral 或者枚舉類型的數據對 static const 類型的數據進行初始化。對於該現象的解釋爲:

If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a constant-initializer which shall be an integral constant expression . In that case, the member can appear in integral constant expressions. The member shall still be defined in a namespace scope if it is used in the program and the namespace scope definition shall not contain an initializer.

什麼是 intergral 類型?

bool、char、wchar_t 包含被 signed 或者 unsigned 修飾的情況,統稱integral類型,integral 類型的同義詞是 integer 類型。

指向類成員的指針

定義指針,使其指向類數據成員或者成員函數,然後通過指針訪問類的成員。

指向類數據成員的指針

定義

datatype classname::*pointer

初始化

datatype classname::*pointer = &classname::non_static

指向非靜態數據成員的指針在定義時必須和類相互關聯,在使用時必須和具體的對象相互關聯。

解引用

使用該種類型的指針時,需要先指定類的一個對象,然後通過對象引用指針指向的成員。

class_obj.*pointer
class_obj_pointer->*pointer

實例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member + this->name + " ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

public:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);

    char * STUDENT::*p;
    p = &STUDENT::name;

    cout<<st.*p<<endl;

    return 0;
}

結果爲:

zhangsan

但是這種使用方式需要打開數據成員的權限,會有點打破封裝的形式。

指向成員函數的指針

非靜態成員函數由三部分構成:

  • 參數列表
  • 返回類型
  • 所屬類型

因此指向該成員函數的指針應該也在這三方面與之保持相同。

定義

datatype (classname::*pointer)(argument list)

初始化

datatype (classname::*pointer)(argument list) = &classname::non_static

解引用

使用該種類型的指針時,需要先指定類的一個對象,然後通過對象引用指針指向的成員。

(class_obj.*pointer)(argument list)
(class_obj_pointer->*pointer)(argument list)

實例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member + this->name + " ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

public:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);

    void (STUDENT::*p)() const;
    p = &STUDENT::display;

    (st.*p)();

    return 0;
}

結果爲:

name is zhangsan
num is 100
grade is 80

該成員函數對應的 const 不要省略。

指向類靜態成員的指針

  • 指向靜態數據成員的指針的定義和使用與普通指針相同,在定義時無須和類相關聯,在使用時也無須和具體的對象相關聯。
  • 指向靜態成員函數的指針和普通指針相同,在定義時無須和類相關聯,在使用時也無須和具體的對象相關聯。
     
#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member + this->name + " ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

public:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);

    string *p = &STUDENT::member;

    cout<<*p<<endl;

    static void (*pp)() = &STUDENT::dismem;

    pp();

    return 0;
}

此時要注意不管是靜態數據成員還是函數成員,在創建指針進行指向的時候,都是採用的是類外定義的形式:

string *p = &STUDENT::member;

static void (*pp)() = &STUDENT::dismem;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章