C++複習二-默認參數-內聯函數-頭文件處理-類與對象

目錄

1.頭文件處理記住:

2.內聯函數:

3.C++函數的默認參數詳解

4.C++函數重載

5.類與對象

塊內容-命名空間

塊內容-函數調用慣例(stdcall,cdecl,pascal等解釋)

塊內容-C語言const的用法詳解

塊內容-二維數組指針

6.構造函數和析構函數

默認構造函數初始化列表:

析構函數的執行時機

封閉類和對象

成員對象的消亡

this指針

7.C++ static靜態成員變量與函數

8.const成員變量、函數、對象

const成員變量

const成員函數(常成員函數)

C++ const對象(常對象)

9.C++友元函數和友元類(C++ friend關鍵字)

友元函數

友元類

10.類其實也是一種作用域

需要記住C++ class和struct到底有什麼區別:


1.頭文件處理記住:

全局變量最好在.c文件裏直接聲明和定義,如果其他文件需要引用,直接extern。

extern不要定義了,否則就重複了。

函數extern或者include貌似都沒什麼問題。

2.內聯函數:

內聯函數inline標誌符是用來實現的,不是用來聲明的;

內聯函數雖然叫做函數,在定義和聲明的語法上也和普通函數一樣,但它已經失去了函數的本質。函數是一段可以重複使用的代碼,它位於虛擬地址空間中的代碼區,也佔用可執行文件的體積,而內聯函數的代碼在編譯後就被消除了,不存在於虛擬地址空間中,沒法重複使用。

函數調用是有時間和空間開銷的。程序在執行一個函數之前需要做一些準備工作,要將實參、局部變量、返回地址以及若干寄存器都壓入棧中,然後才能執行函數體中的代碼;函數體中的代碼執行完畢後還要清理現場,將之前壓入棧中的數據都出棧,才能接着執行函數調用位置以後的代碼。

如果函數體代碼比較多,需要較長的執行時間,那麼函數調用機制佔用的時間可以忽略;如果函數只有一兩條語句,那麼大部分的時間都會花費在函數調用機制上,這種時間開銷就就不容忽視。

使用內聯函數的缺點也是非常明顯的,編譯後的程序會存在多份相同的函數拷貝,如果被聲明爲內聯函數的函數體非常大,那麼編譯後的程序體積也將會變得很大,所以再次強調,一般只將那些短小的、頻繁調用的函數聲明爲內聯函數

由於內聯函數比較短小,我們通常的做法是省略函數原型,將整個函數定義(包括函數頭和函數體)放在本應該提供函數原型的地方。

宏定義是一項“細思極密”的工作,一不小心就會踩坑,而且不一定在編譯和運行時發現,給程序埋下隱患。

如果我們將宏替換爲內聯函數,情況就沒有那麼複雜了,程序員就會遊刃有餘,

3.C++函數的默認參數詳解

所謂默認參數,指的是當函數調用中省略了實參時自動使用的一個值,這個值就是給形參指定的默認值。

#include<iostream>
using namespace std;

//帶默認參數的函數
void func(int n, float b=1.2, char c='@'){
    cout<<n<<", "<<b<<", "<<c<<endl;
}

int main(){
    //爲所有參數傳值
    func(10, 3.5, '#');
    //爲n、b傳值,相當於調用func(20, 9.8, '@')
    func(20, 9.8);
    //只爲n傳值,相當於調用func(30, 1.2, '@')
    func(30);

    return 0;
}

默認參數除了使用數值常量指定,也可以使用表達式指定;

float d = 10.8;
void func(int n, float b=d+2.9, char c='@'){
    cout<<n<<", "<<b<<", "<<c<<endl;
}

C++規定,默認參數只能放在形參列表的最後,而且一旦爲某個形參指定了默認值,那麼它後面的所有形參都必須有默認值。實參和形參的傳值是從左到右依次匹配的,默認參數的連續性是保證正確傳參的前提。

我們在函數定義處指定了默認參數。除了函數定義,你也可以在函數聲明處指定默認參數。不過當出現函數聲明時情況會變得稍微複雜。

因爲C++ 規定,在給定的作用域中只能指定一次默認參數。
C和C++語言有四種作用域,分別是函數原型作用域、局部作用域(函數作用域)、塊作用域、文件作用域(全局作用域)。

所以函數原型定義在一個文件,函數聲明在另一個文件就好了,兩個文件的函數默認參數都可以指定,以發起調用的主函數的爲主

默認參數劃重點部分:多次聲明默認參數(同一函數)

4.C++函數重載

在實際開發中,有時候我們需要實現幾個功能類似的函數,只是有些細節不同。例如希望交換兩個變量的值,這兩個變量有多種類型,可以是 int、float、char、bool 等,我們需要通過參數把變量的地址傳入函數內部。在C語言中,程序員往往需要分別設計出三個不同名的函數,其函數原型與下面類似:

void swap1(int *a, int *b); //交換 int 變量的值
void swap2(float *a, float *b); //交換 float 變量的值
void swap3(char *a, char *b); //交換 char 變量的值
void swap4(bool *a, bool *b); //交換 bool 變量的值

但在C++中,這完全沒有必要。C++ 允許多個函數擁有相同的名字,只要它們的參數列表不同就可以,這就是函數的重載(Function Overloading)。藉助重載,一個函數名可以有多種用途。

參數列表又叫參數簽名,包括參數的類型、參數的個數和參數的順序,只要有一個不同就叫做參數列表不同。

函數返回值也不能作爲重載的依據。

後面直接用模板類解決更方便。

5.類與對象

類只是一個模板(Template),編譯後不佔用內存空間,所以在定義類時不能對成員變量進行初始化,因爲沒有地方存儲數據。只有在創建對象以後纔會給成員變量分配內存,這個時候就可以賦值了。

創建對象:

Student stu1;
stu1.name="test";

int a=1234;
char name[]="test";
Student*stu2=new Student(name,a);
Student*stu3=new Student;

delete stu2;
delete stu3;

Student*stu4=&stu1;

::被稱爲域解析符(也稱作用域運算符或作用域限定符),用來連接類名和函數名,指明當前函數屬於哪個類。

成員函數必須先在類體中作原型聲明,然後在類外定義,也就是說類體的位置應在函數定義之前。

在類體中和類體外定義成員函數是有區別的:在類體中定義的成員函數會自動成爲內聯函數,在類體外定義的不會。當然,在類體內部定義的函數也可以加 inline 關鍵字,但這是多餘的,因爲類體內部定義的函數默認就是內聯函數。

內聯函數一般不是我們所期望的,它會將函數調用處用函數體替代,所以我建議在類體內部對成員函數作聲明,而在類體外部進行定義,這是一種良好的編程習慣,實際開發中大家也是這樣做的。


C++通過 public、protected、private 三個關鍵字來控制成員變量和成員函數的訪問權限,它們分別表示公有的、受保護的、私有的,被稱爲成員訪問限定符。所謂訪問權限,就是你能不能使用該類中的成員。

JavaC# 程序員注意,C++ 中的 public、private、protected 只能修飾類的成員,不能修飾類,C++中的類沒有共有私有之分。


成員變量大都以m_開頭,這是約定成俗的寫法,不是語法規定的內容。以m_開頭既可以一眼看出這是成員變量,又可以和成員函數中的形參名字區分開。

以 setname() 爲例,如果將成員變量m_name的名字修改爲name,那麼 setname() 的形參就不能再叫name了,得換成諸如name1_name這樣沒有明顯含義的名字,否則name=name;這樣的語句就是給形參name賦值,而不是給成員變量name賦值。

根據C++軟件設計規範,實際項目開發中的成員變量以及只在類內部使用的成員函數(只被成員函數調用的成員函數)都建議聲明爲 private,而只將允許通過對象調用的成員函數聲明爲 public。

另外還有一個關鍵字 protected,聲明爲 protected 的成員在類外也不能通過對象訪問,但是在它的派生類內部可以訪問。

在一個類體中,private 和 public 可以分別出現多次。每個部分的有效範圍到出現另一個訪問限定符或類體結束時(最後一個右花括號)爲止。但是爲了使程序清晰,應該養成這樣的習慣,使每一種成員訪問限定符在類定義體中只出現一次。

類是創建對象的模板,不佔用內存空間,不存在於編譯後的可執行文件中;而對象是實實在在的數據,需要內存來存儲。對象被創建時會在棧區或者堆區分配內存。

直觀的認識是,如果創建了 10 個對象,就要分別爲這 10 個對象的成員變量和成員函數分配內存。

編譯器會將成員變量和成員函數分開存儲:分別爲每個對象的成員變量分配內存,但是所有對象都共享同一段函數代碼。

成員變量在堆區或棧區分配內存,成員函數在代碼區分配內存。

    //在棧上創建對象
    Student stu;
    cout<<sizeof(stu)<<endl;
    //在堆上創建對象
    Student *pstu = new Student();
    cout<<sizeof(*pstu)<<endl;
    //類的大小
    cout<<sizeof(Student)<<endl;

Student 類包含三個成員變量,它們的類型分別是 char *、int、float,都佔用 4 個字節的內存,加起來共佔用 12 個字節的內存。通過 sizeof 求得的結果等於 12,恰好說明對象所佔用的內存僅僅包含了成員變量。

類可以看做是一種複雜的數據類型,也可以使用 sizeof 求得該類型的大小。從運行結果可以看出,在計算類這種類型的大小時,只計算了成員變量的大小,並沒有把成員函數也包含在內。

對象的大小隻受成員變量的影響,和成員函數沒有關係。

假設 stu 的起始地址爲 0X1000,那麼該對象的內存分佈如下圖所示:

塊內容-命名空間

塊內容-函數調用慣例(stdcall,cdecl,pascal等解釋)

{

 

調用慣例 參數傳遞方式 參數出棧方式 名字修飾
cdecl 按照從右到左的順序入棧 調用方 下劃線+函數名,
如函數 max() 的修飾名爲 _max
stdcall 按照從右到左的順序入棧 函數本身
(被調用方)
下劃線+函數名+@+參數的字節數,
如函數 int max(int m, int n) 的修飾名爲 _max_@8
fastcall 將部分參數放入寄存器,
剩下的參數按照從右到左的順序入棧
函數本身
(被調用方)
@+函數名+@+參數的字節數
pascal 按照從左到右的順序入棧 函數本身
(被調用方)
較爲複雜,這裏不再展開討論
 

}

塊內容-C語言const的用法詳解

塊內容-二維數組指針

{

#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));
    return 0;
}

運行結果:16

*(p+1)相當於代表a的第二個小數組的數組名,也就是說是個地址;就像一維數組的名字,在定義時或者和 sizeof、& 一起使用時才表示整個數組,出現在表達式中就會被轉換爲指向數組第 0 個元素的指針。

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

}

6.構造函數和析構函數

構造函數必須是 public 屬性的,否則創建對象時無法調用。

構造函數沒有返回值,因爲沒有變量來接收返回值,即使有也毫無用處,這意味着:

  • 不管是聲明還是定義,函數名前面都不能出現返回值類型,即使是 void 也不允許;
  • 函數體中不能有 return 語句。

構造函數一旦自行設置創建對象就一定要調用,因爲自行設置後系統提供的默認構造函數失效了。

調用沒有參數的構造函數也可以省略括號。在棧上創建對象可以寫作Student stu()Student stu,在堆上創建對象可以寫作Student *pstu = new Student()Student *pstu = new Student,它們都會調用構造函數 Student()。

默認構造函數初始化列表:

//採用初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    //TODO:
}

注意,成員變量的初始化順序與初始化列表中列出的變量的順序無關,它只與成員變量在類中聲明的順序有關。

Demo::Demo(int b): m_b(b), m_a(m_b){ }
在初始化列表中,我們將 m_b 放在了 m_a 的前面,看起來是先給 m_b 賦值,再給 m_a 賦值,其實不然!
成員變量的賦值順序由它們在類中的聲明順序決定,在 Demo 類中,我們先聲明的 m_a,再聲明的 m_b,
所以構造函數和下面的代碼等價:

Demo::Demo(int b): m_b(b), m_a(m_b){
    m_a = m_b;
    m_b = b;
}

初始化 const 成員變量的唯一方法就是使用初始化列表。

class VLA{
private:
    const int m_len;
    int *m_arr;
public:
    VLA(int len);
};

//必須使用初始化列表來初始化 m_len
VLA::VLA(int len): m_len(len){
    m_arr = new int[len];
}

析構函數(Destructor)也是一種特殊的成員函數,沒有返回值,不需要程序員顯式調用(程序員也沒法顯式調用),而是在銷燬對象時自動執行。

析構函數的執行時機

析構函數在對象被銷燬時調用,而對象的銷燬時機與它所在的內存區域有關。不瞭解內存分區的讀者請閱讀《C語言內存精講》專題。

在所有函數之外創建的對象是全局對象,它和全局變量類似,位於內存分區中的全局數據區,程序在結束執行時會調用這些對象的析構函數。

在函數內部創建的對象是局部對象,它和局部變量類似,位於棧區,函數執行結束時會調用這些對象的析構函數。

new 創建的對象位於堆區,通過 delete 刪除時纔會調用析構函數;如果沒有 delete,析構函數就不會被執行。

#include <iostream>
#include <string>
using namespace std;
class Demo{
public:
    Demo(string s);
    ~Demo();
private:
    string m_s;
};
Demo::Demo(string s): m_s(s){ }
Demo::~Demo(){ cout<<m_s<<endl; }
void func(){
    //局部對象
    Demo obj1("1");
}
//全局對象
Demo obj2("2");
int main(){
    //局部對象
    Demo obj3("3");
    //new創建的對象
    Demo *pobj4 = new Demo("4");
    func();
    cout<<"main"<<endl;
  
    return 0;
}
輸出:
1
main
3
2

封閉類和對象

#include <iostream>
using namespace std;
//輪胎類
class Tyre{
public:
    Tyre(int radius, int width);
    void show() const;
private:
    int m_radius;  //半徑
    int m_width;  //寬度
};
Tyre::Tyre(int radius, int width) : m_radius(radius), m_width(width){ }
void Tyre::show() const {
    cout << "輪轂半徑:" << this->m_radius << "吋" << endl;
    cout << "輪胎寬度:" << this->m_width << "mm" << endl;
}
//引擎類
class Engine{
public:
    Engine(float displacement = 2.0);
    void show() const;
private:
    float m_displacement;
};
Engine::Engine(float displacement) : m_displacement(displacement) {}
void Engine::show() const {
    cout << "排量:" << this->m_displacement << "L" << endl;
}
//汽車類
class Car{
public:
    Car(int price, int radius, int width);
    void show() const;
private:
    int m_price;  //價格
    Tyre m_tyre;
    Engine m_engine;
};
Car::Car(int price, int radius, int width): m_price(price), m_tyre(radius, width)/*指明m_tyre對象的初始化方式*/{ };
void Car::show() const {
    cout << "價格:" << this->m_price << "¥" << endl;
    this->m_tyre.show();
    this->m_engine.show();
}
int main()
{
    Car car(200000, 19, 245);
    car.show();
    return 0;
}
運行結果:
價格:200000¥
輪轂直徑:19吋
輪胎寬度:245mm
排量:2L


Car 是一個封閉類,它有兩個成員對象:m_tyre 和 m_engine。在編譯第 51 行時,編譯器需要知道 car 對象中的 m_tyre 和 m_engine 成員對象該如何初始化。

編評器已經知道這裏的 car 對象是用第 42 行的 Car(int price, int radius, int width) 構造函數初始化的,那麼 m_tyre 和 m_engine 該如何初始化,就要看第 42 行後面的初始化列表了。該初始化列表表明:

  • m_tyre 應以 radius 和 width 作爲參數調用 Tyre(int radius, int width) 構造函數初始化。
  • 但是這裏並沒有說明 m_engine 該如何處理。在這種情況下,編譯器就認爲 m_engine 應該用 Engine 類的無參構造函數初始化。而 Engine 類確實有一個無參構造函數(因爲設置了默認參數),因此,整個 car 對象的初始化問題就都解決了。


總之,生成封閉類對象的語句一定要讓編譯器能夠弄明白其成員對象是如何初始化的,否則就會編譯錯誤。

在上面的程序中,如果 Car 類的構造函數沒有初始化列表,那麼第 51 行就會編譯出錯,因爲編譯器不知道該如何初始化 car.m_tyre 對象,因爲 Tyre 類沒有無參構造函數,而編譯器又找不到用來初始化 car.m_tyre 對象的參數。

成員對象的消亡

封閉類對象生成時,先執行所有成員對象的構造函數,然後才執行封閉類自己的構造函數。成員對象構造函數的執行次序和成員對象在類定義中的次序一致,與它們在構造函數初始化列表中出現的次序無關。

當封閉類對象消亡時,先執行封閉類的析構函數,然後再執行成員對象的析構函數,成員對象析構函數的執行次序和構造函數的執行次序相反,即先構造的後析構,這是 C++ 處理此類次序問題的一般規律。

this指針

this 代表着當前調用類的對象的地址,只能用在類的內部

通過 this 可以訪問類的所有成員,包括 private、protected、public 屬性的。

本例中成員函數的參數和成員變量重名,只能通過 this 區分。以成員函數setname(char *name)爲例,它的形參是name,和成員變量name重名,如果寫作name = name;這樣的語句,就是給形參name賦值,而不是給成員變量name賦值。而寫作this -> name = name;後,=左邊的name就是成員變量,右邊的name就是形參,一目瞭然。

注意,this 是一個指針,要用->來訪問成員變量或成員函數。

this 雖然用在類的內部,但是隻有在對象被創建以後纔會給 this 賦值,並且這個賦值的過程是編譯器自動完成的,不需要用戶干預,用戶也不能顯式地給 this 賦值。本例中,this 的值和 pstu 的值是相同的。

this 實際上是成員函數的一個形參,在調用成員函數時將對象的地址作爲實參傳遞給 this。不過 this 這個形參是隱式的,它並不出現在代碼中,而是在編譯階段由編譯器默默地將它添加到參數列表中。

7.C++ static靜態成員變量與函數

static 成員變量的內存既不是在聲明類時分配,也不是在創建對象時分配,而是在(類外)初始化時分配。

反過來說,沒有在類外初始化的 static 成員變量不能使用。

class Student{
public:
    Student(char *name, int age, float score);
    void show();
public:
    static int m_total;  //靜態成員變量
private:
    char *m_name;
    int m_age;
    float m_score;
};

初始化:

static 成員變量必須在類聲明的外部初始化,具體形式爲:
type class::name = value;

type 是變量的類型,class 是類名,name 是變量名,value 是初始值。將上面的 m_total 初始化:
int Student::m_total = 0;

靜態成員變量在初始化時不能再加 static,但必須要有數據類型。
被 private、protected、public 修飾的靜態成員變量都可以用這種方式初始化。

訪問:

//通過類類訪問 static 成員變量
Student::m_total = 10;
//通過對象來訪問 static 成員變量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通過對象指針來訪問 static 成員變量
Student *pstu = new Student("李華", 16, 96);
pstu -> m_total = 20;



int main(){
    //創建匿名對象
    (new Student("小明", 15, 90)) -> show();
    (new Student("李磊", 16, 80)) -> show();
    (new Student("張華", 16, 99)) -> show();
    (new Student("王康", 14, 60)) -> show();
    return 0;
}

1) 一個類中可以有一個或多個靜態成員變量,所有的對象都共享這些靜態成員變量,都可以引用它。

2) static 成員變量和普通 static 變量一樣,都在內存分區中的全局數據區分配內存,到程序結束時才釋放。這就意味着,static 成員變量不隨對象的創建而分配內存,也不隨對象的銷燬而釋放內存。而普通成員變量在對象創建時分配內存,在對象銷燬時釋放內存。

3) 靜態成員變量必須初始化,而且只能在類體外進行。例如:

int Student::m_total = 10;

初始化時可以賦初值,也可以不賦值。如果不賦值,那麼會被默認初始化爲 0。全局數據區的變量都有默認的初始值 0,而動態數據區(堆區、棧區)變量的默認值是不確定的,一般認爲是垃圾值。

4) 靜態成員變量既可以通過對象名訪問,也可以通過類名訪問,但要遵循 private、protected 和 public 關鍵字的訪問權限限制。當通過對象名訪問時,對於不同的對象,訪問的是同一份內存。

靜態成員函數與普通成員函數的根本區別在於:普通成員函數有 this 指針,可以訪問類中的任意成員;而靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。

和靜態成員變量類似,靜態成員函數在聲明時要加 static,在定義時不能加 static。靜態成員函數可以通過類來調用(一般都是這樣做),也可以通過對象來調用。

#include <iostream>
using namespace std;

class Student{
public:
    Student(char *name, int age, float score);
    void show();
public:  //聲明靜態成員函數
    static int getTotal();
    static float getPoints();
private:
    static int m_total;  //總人數
    static float m_points;  //總成績
private:
    char *m_name;
    int m_age;
    float m_score;
};

int Student::m_total = 0;
float Student::m_points = 0.0;

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    m_total++;
    m_points += score;
}
void Student::show(){
    cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
//定義靜態成員函數
int Student::getTotal(){
    return m_total;
}
float Student::getPoints(){
    return m_points;
}

int main(){
    (new Student("小明", 15, 90.6)) -> show();
    (new Student("李磊", 16, 80.5)) -> show();
    (new Student("張華", 16, 99.0)) -> show();
    (new Student("王康", 14, 60.8)) -> show();

    int total = Student::getTotal();
    float points = Student::getPoints();
    cout<<"當前共有"<<total<<"名學生,總成績是"<<points<<",平均分是"<<points/total<<endl;

    return 0;
}

8.const成員變量、函數、對象

const成員變量

const 成員變量的用法和普通 const 變量的用法相似,只需要在聲明時加上 const 關鍵字。初始化 const 成員變量只有一種方法,就是通過構造函數的初始化列表。

const成員函數(常成員函數)

const 成員函數可以使用類中的所有成員變量,但是不能修改它們的值,這種措施主要還是爲了保護數據而設置的。const 成員函數也稱爲常成員函數。

我們通常將 get 函數設置爲常成員函數。讀取成員變量的函數的名字通常以get開頭,後跟成員變量的名字,所以通常將它們稱爲 get 函數。

常成員函數需要在聲明和定義的時候在函數頭部的結尾加上 const 關鍵字,請看下面的例子:

class Student{
public:
    Student(char *name, int age, float score);
    void show();
    //聲明常成員函數
    char *getname() const;
    int getage() const;
    float getscore() const;
private:
    char *m_name;
    int m_age;
    float m_score;
};

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
    cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
//定義常成員函數
char * Student::getname() const{
    return m_name;
}
int Student::getage() const{
    return m_age;
}
float Student::getscore() const{
    return m_score;
}

最後再來區分一下 const 的位置:

  • 函數開頭的 const 用來修飾函數的返回值,表示返回值是 const 類型,也就是不能被修改,例如const char * getname()
  • 函數頭部的結尾加上 const 表示常成員函數,這種函數只能讀取成員變量的值,而不能修改成員變量的值,例如char * getname() const

C++ const對象(常對象)

在 C++ 中,const 也可以用來修飾對象,稱爲常對象。一旦將對象定義爲常對象之後,就只能調用類的 const 成員(包括 const 成員變量和 const 成員函數)了。

定義常對象的語法和定義常量的語法類似:

const  class  object(params);
class const object(params);

當然你也可以定義 const 指針

const class *p = new class(params);
class const *p = new class(params);

9.C++友元函數和友元類(C++ friend關鍵字)

友元函數

在 C++ 中,一個類中可以有 public、protected、private 三種屬性的成員,通過對象可以訪問 public 成員,只有本類中的函數可以訪問本類的 private 成員。現在,我們來介紹一種例外情況——友元(friend)。藉助友元(friend),可以使得其他類中的成員函數以及全局範圍內的函數訪問當前類的 private 成員。

在當前類以外定義的、不屬於當前類的函數也可以在類中聲明,但要在前面加 friend 關鍵字,這樣就構成了友元函數。友元函數可以是不屬於任何類的非成員函數,也可以是其他類的成員函數。

友元函數可以訪問當前類中的所有成員,包括 public、protected、private 屬性的。

1) 將非成員函數聲明爲友元函數

注意,友元函數不同於類的成員函數,在友元函數中不能直接訪問類的成員,必須要藉助對象。

#include <iostream>
using namespace std;
class Student{
public:
    Student(char *name, int age, float score);
public:
    friend void show(Student *pstu);  //將show()聲明爲友元函數
private:
    char *m_name;
    int m_age;
    float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//非成員函數
void show(Student *pstu){
    cout<<pstu->m_name<<"的年齡是 "<<pstu->m_age<<",成績是 "<<pstu->m_score<<endl;
}
int main(){
    Student stu("小明", 15, 90.6);
    show(&stu);  //調用友元函數
    Student *pstu = new Student("李磊", 16, 80.5);
    show(pstu);  //調用友元函數
    return 0;
}
運行結果:
小明的年齡是 15,成績是 90.6
李磊的年齡是 16,成績是 80.5

注意,友元函數不同於類的成員函數,在友元函數中不能直接訪問類的成員,必須要藉助對象。

下面的寫法是錯誤的:
純文本複製
void show(){
    cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
}

2) 將其他類的成員函數聲明爲友元函數

friend 函數不僅可以是全局函數(非成員函數),還可以是另外一個類的成員函數。

注意,友元函數不同於類的成員函數,在友元函數中不能直接訪問類的成員,必須要藉助對象。請看下面的例子:

#include <iostream>
using namespace std;

class Address;  //提前聲明Address類

//聲明Student類
class Student{
public:
    Student(char *name, int age, float score);
public:
    void show(Address *addr);
private:
    char *m_name;
    int m_age;
    float m_score;
};

//聲明Address類
class Address{
private:
    char *m_province;  //省份
    char *m_city;  //城市
    char *m_district;  //區(市區)
public:
    Address(char *province, char *city, char *district);
    //將Student類中的成員函數show()聲明爲友元函數
    friend void Student::show(Address *addr);
};

//實現Student類
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){
    cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
    cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"區"<<endl;
}

//實現Address類
Address::Address(char *province, char *city, char *district){
    m_province = province;
    m_city = city;
    m_district = district;
}

int main(){
    Student stu("小明", 16, 95.5f);
    Address addr("陝西", "西安", "雁塔");
    stu.show(&addr);
   
    Student *pstu = new Student("李磊", 16, 80.5);
    Address *paddr = new Address("河北", "衡水", "桃城");
    pstu -> show(paddr);

    return 0;
}

幾點注意:
① 程序第 4 行對 Address 類進行了提前聲明,是因爲在 Address 類定義之前、在 Student 類中使用到了它,如果不提前聲明,編譯器會報錯,提示'Address' has not been declared類的提前聲明和函數的提前聲明是一個道理。

② 程序將 Student 類的聲明和實現分開了,而將 Address 類的聲明放在了中間,這是因爲編譯器從上到下編譯代碼,show() 函數體中用到了 Address 的成員 province、city、district,如果提前不知道 Address 的具體聲明內容,就不能確定 Address 是否擁有該成員(類的聲明中指明瞭類有哪些成員)。

這裏簡單介紹一下類的提前聲明。一般情況下,類必須在正式聲明之後才能使用;但是某些情況下(如上例所示),只要做好提前聲明,也可以先使用。

但是應當注意,類的提前聲明的使用範圍是有限的,只有在正式聲明一個類以後才能用它去創建對象。如果在上面程序的第4行之後增加如下所示的一條語句,編譯器就會報錯:

Address addr;  //企圖使用不完整的類來創建對象

因爲創建對象時要爲對象分配內存,在正式聲明類之前,編譯器無法確定應該爲對象分配多大的內存。編譯器只有在“見到”類的正式聲明後(其實是見到成員變量),才能確定應該爲對象預留多大的內存。在對一個類作了提前聲明後,可以用該類的名字去定義指向該類型對象的指針變量(本例就定義了 Address 類的指針變量)或引用變量(後續會介紹引用),因爲指針變量和引用變量本身的大小是固定的,與它所指向的數據的大小無關。

③ 一個函數可以被多個類聲明爲友元函數,這樣就可以訪問多個類中的 private 成員。

友元類

不僅可以將一個函數聲明爲一個類的“朋友”,還可以將整個類聲明爲另一個類的“朋友”,這就是友元類。友元類中的所有成員函數都是另外一個類的友元函數。

例如將類 B 聲明爲類 A 的友元類,那麼類 B 中的所有成員函數都是類 A 的友元函數,可以訪問類 A 的所有成員,包括 public、protected、private 屬性的。

更改上例的代碼,將 Student 類聲明爲 Address 類的友元類:

#include <iostream>
using namespace std;

class Address;  //提前聲明Address類

//聲明Student類
class Student{
public:
    Student(char *name, int age, float score);
public:
    void show(Address *addr);
private:
    char *m_name;
    int m_age;
    float m_score;
};

//聲明Address類
class Address{
public:
    Address(char *province, char *city, char *district);
public:
    //將Student類聲明爲Address類的友元類
    friend class Student;
private:
    char *m_province;  //省份
    char *m_city;  //城市
    char *m_district;  //區(市區)
};

//實現Student類
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){
    cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
    cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"區"<<endl;
}

//實現Address類
Address::Address(char *province, char *city, char *district){
    m_province = province;
    m_city = city;
    m_district = district;
}

int main(){
    Student stu("小明", 16, 95.5f);
    Address addr("陝西", "西安", "雁塔");
    stu.show(&addr);
   
    Student *pstu = new Student("李磊", 16, 80.5);
    Address *paddr = new Address("河北", "衡水", "桃城");
    pstu -> show(paddr);

    return 0;
}

關於友元,有兩點需要說明:

  • 友元的關係是單向的而不是雙向的。如果聲明瞭類 B 是類 A 的友元類,不等於類 A 是類 B 的友元類,類 A 中的成員函數不能訪問類 B 中的 private 成員。
  • 友元的關係不能傳遞。如果類 B 是類 A 的友元類,類 C 是類 B 的友元類,不等於類 C 是類 A 的友元類。

除非有必要,一般不建議把整個類聲明爲友元類,而只將某些成員函數聲明爲友元函數,這樣更安全一些。

10.類其實也是一種作用域

#include<iostream>
using namespace std;
class A{
public:
    typedef int INT;
    static void show();
    void work();
};
void A::show(){ cout<<"show()"<<endl; }
void A::work(){ cout<<"work()"<<endl; }
int main(){
    A a;
    a.work();  //通過對象訪問普通成員
    a.show();  //通過對象訪問靜態成員
    A::show();  //通過類訪問靜態成員
    A::INT n = 10;  //通過類訪問 typedef 定義的類型
    return 0;
}
對的:
void A::show(PCHAR str){
    cout<<str<<endl;
    n = 10;
    return str;
}
錯的:
PCHAR A::show(PCHAR str){
    cout<<str<<endl;
    n = 10;
    return str;
}
對的:
A::PCHAR A::show(PCHAR str){
    cout<<str<<endl;
    n = 10;
    return str;
}

 

需要記住C++ class和struct到底有什麼區別:

C++中的 struct 和 class 基本是通用的,唯有幾個細節不同:

  • 使用 class 時,類中的成員默認都是 private 屬性的;而使用 struct 時,結構體中的成員默認都是 public 屬性的。
  • class 繼承默認是 private 繼承,而 struct 繼承默認是 public 繼承(《C++繼承與派生》一章會講解繼承)。
  • class 可以使用模板,而 struct 不能(《模板、字符串和異常》一章會講解模板)。

在編寫C++代碼時,我強烈建議使用 class 來定義類,而使用 struct 來定義結構體,這樣做語義更加明確。

但是下面看下C++的struct結構體變化:

#include <iostream>
using namespace std;

struct Student{
    Student(char *name, int age, float score);
    void show();

    char *m_name;
    int m_age;
    float m_score;
};

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
    cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}

int main(){
    Student stu("小明", 15, 92.5f);
    stu.show();
    Student *pstu = new Student("李華", 16, 96);
    pstu -> show();

    return 0;
}


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章