国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

這篇具有很好參考價值的文章主要介紹了【C++篇】OOP下部分:友元、運算符重載與多態(tài)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

友情鏈接:C/C++系列系統(tǒng)學習目錄

知識總結(jié)順序參考C Primer Plus(第六版)和譚浩強老師的C程序設計(第五版)等,內(nèi)容以書中為標準,同時參考其它各類書籍以及優(yōu)質(zhì)文章,以至減少知識點上的錯誤,同時方便本人的基礎復習,也希望能幫助到大家
?
最好的好人,都是犯過錯誤的過來人;一個人往往因為有一點小小的缺點,將來會變得更好。如有錯漏之處,敬請指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上



??一、友元

C++是面向?qū)ο蟮?,目的之一:封裝

優(yōu)點:優(yōu)點之一,就是安全。

缺點:在某些特殊的場合,不是很方便

解決方案:使用友元

使用前提:某個類需要實現(xiàn)某種功能,但是這個類自身,因為各種原因,無法自己實現(xiàn)。需要借助于“外力”才能實現(xiàn)。

友元有兩種使用形式:友元函數(shù)和友元類。

?(一)友元函數(shù)

使用全局函數(shù)作為友元函數(shù)(友元函數(shù))

通過讓函數(shù)成為類的友元,可以賦予該函數(shù)與類的成員函數(shù)相同的訪問權限,創(chuàng)建友元函數(shù)的第一步是將其原型放在類聲明中,并在原型聲明前加上關鍵字friend:

  • 第一,雖然upgrade()函數(shù)是在類聲明中聲明的,但它不是成員函數(shù),因此不能使用成員運算符來調(diào)用;
  • 第二,upgrade()函數(shù)不是成員函數(shù),但它與成員函數(shù)的訪問權限相同。
Computer.h
class Computer
{
public:
    Computer();
    
    // 使用全局函數(shù)作為友元函數(shù)
    friend void upgrade(Computer* computer);
    
    std::string description();
    
private:
    std::string cpu; //CPU芯片
};

Computer.cpp
Computer::Computer()
{
	cpu = "i7";
}

main.cpp
 //它不是成員函數(shù),所以不要使用Computer::限定符,另外,不要在定義中使用關鍵字friend
void upgrade(Computer* computer) {
	computer->cpu = "i9"; //非成員函數(shù)不能直接訪問類的私有數(shù)據(jù),至少常規(guī)非成員函數(shù)不能訪問。然而,有一類特殊的非成員函數(shù)可以訪問類的私有成員,它們被稱為友元函數(shù)。這樣就可像成員函數(shù)一樣直接訪問對象的私有數(shù)據(jù)成員
}    

int main(void) {
    
    upgrade(&shanxing);
    
    std::cout << shanxing.description() << std::endl;
    
    system("pause");
    return 0;
}

使用另一個類的成員函數(shù)作為友元函數(shù)(友元成員函數(shù))

Computer.h
#pragma once
#include <string>

// class ComputerService;
// 僅僅聲明ComputerService不夠,需要包含頭文件,因為要使用類當中的方法
#include "ComputerService.h"

class Computer
{
public:
    Computer();
    
    // 使用全局函數(shù)作為友元函數(shù)
    friend void upgrade(Computer* computer);
    
    // 使用類的成員函數(shù),作為友元函數(shù)
    friend void ComputerService::upgrade(Computer* comptuer);
    
    std::string description();
    
private:
	std::string cpu; //CPU芯片
};

ComputerService.h
#pragma once
class Computer;

class ComputerService
{
public:
	void upgrade(Computer* computer);
};

ComputerService.cpp
#include "ComputerService.h"
#include "Computer.h"
    
void ComputerService::upgrade(Computer* computer) {
	computer->cpu = "i9";
}

main.cpp
#include <stdio.h>
#include <iostream>
#include <Windows.h>
#include "Computer.h"
#include "ComputerService.h"
int main(void) {
    Computer shanxing;
    ComputerService service;
    
    std::cout << shanxing.description() << std::endl;
    
    service.upgrade(&shanxing);
    
    std::cout << shanxing.description() << std::endl;
    
    system("pause");
    return 0;
}

功能上,這兩種形式,都是相同,應用場合不同。

一個是,使用普通的全局函數(shù),作為自己的朋友,實現(xiàn)特殊功能。

一個是,使用其他類的成員函數(shù),作為自己的朋友,實現(xiàn)特殊功能。

?(二)友元類

為什么要使用友元類

一個獨立的咨詢師, 給其他企業(yè)做服務時,這個咨詢師作為企業(yè)的“友元函數(shù)”即可。

一個大型的咨詢服務公司,比如 IBM(IT 事務), 普華永道(會計事務),給其他企業(yè)做服務時,使用友元函數(shù)就不是很方便了,因為需要設計很多友元函數(shù),不方便。

解決方案:使用“友元類”

友元類的作用

如果把 A 類作為 B 類的友元類,

那么 A 類的所有成員函數(shù)【在 A 類的成員函數(shù)內(nèi)】,就可以直接訪問【使用】B 類的私有成員。即,友元類可以直接訪問對應類的所有成員!?。?/p>

使用注意

友元類,和友元函數(shù),使用 friend 關鍵字進行聲明即可,與訪問權限無關,所以,可以放在 private/pulic/protected 任意區(qū)域內(nèi)。

Computer.h

#pragma once
#include <string>

class ComputerService;

class Computer
{
public:
    Computer();
    std::string description();
    
private:
    std::string cpu; //CPU芯片
    
    // 友元類
    friend class ComputerService;
};

Computer.cpp

#include "Computer.h"
#include <sstream>
Computer::Computer()
{
	cpu = "i7";
}

std::string Computer::description()
{
    std::stringstream ret;
    ret << "CPU:" << cpu;
    return ret.str();
}

ComputerService.h

#pragma once

class Computer;

class ComputerService
{
public:
    void upgrade(Computer* computer);
    void clean(Computer* computer); //計算機清理
    void kill(Computer* computer); //殺毒
};

ComputerService.cpp

#include "ComputerService.h"
#include "Computer.h"
#include <iostream>

void ComputerService::upgrade(Computer* computer) {
	computer->cpu = "i9";
}

void ComputerService::clean(Computer* computer)
{
    std::cout << "正在對電腦執(zhí)行清理[CPU:"
    << computer->cpu << "]..."
    << std::endl;
}
void ComputerService::kill(Computer* computer)
{
    std::cout << "正在對電腦執(zhí)行殺毒[CPU:"
    << computer->cpu << "]..."
    << std::endl;
}

main.cpp

#include <stdio.h>
#include <iostream>
#include <Windows.h>
#include "Computer.h"
#include "ComputerService.h"

int main(void) {
    Computer shanxing;
    ComputerService service;
    
    std::cout << shanxing.description() << std::endl;
    
    service.upgrade(&shanxing);
    service.clean(&shanxing);
    service.kill(&shanxing);
    
    std::cout << shanxing.description() << std::endl;
    system("pause");
    return 0;
}

??二、運算符重載

為什么要使用運算符重載

C/C++的運算符,支持的數(shù)據(jù)類型,僅限于基本數(shù)據(jù)類型。

問題:

一頭牛+一頭馬 = ?(牛馬神獸?)

一個圓 +一個圓 = ? (想要變成一個更大的圓)

一頭牛 – 一只羊 = ? (想要變成 4 只羊,原始的以物易物:1 頭牛價值 5 只羊)

解決方案:

使用運算符重載,運算符重載是一種形式的C++多態(tài),實際上,很多C++(也包括C語言)運算符已經(jīng)被重載。例如,將*運算符用于地址,將得到存儲在這個地址中的值;但將它用于兩個數(shù)字時,得到的將是它們的乘積。C++根據(jù)操作數(shù)的數(shù)目和類型來決定采用哪種操作。

?(一)運算符重載的基本用法

??1.使用成員函數(shù)重載運算符

要重載運算符,需使用被稱為運算符函數(shù)的特殊函數(shù)形式。運算符函數(shù)的格式如下:

operatorop(argument-list)

例如,operator +( )重載+運算符,operator*( )重載*運算符。op必須是有效的C++運算符,不能虛構一個新的符號。例如,不能有operator@( )這樣的函數(shù),因為C++中沒有@運算符。operator[ ]函數(shù)將重載[ ]運算符,因為[ ]是數(shù)組索引運算符

district2 = sid + sara;
//編譯器發(fā)現(xiàn),操作數(shù)是Salesperson類對象,因此使用相應的運算符函數(shù)替換上述運算符:
district2 = sid.operator+ (sara) ;

這兩種表示法都將調(diào)用operator +( )方法。注意,在運算符表示法中,運算符左側(cè)的對象(這里為sid)是調(diào)用對象,運算符右邊的對象(這里為sara)是作為參數(shù)被傳遞的對象。函數(shù)將隱式地使用sid(因為它調(diào)用了方法),而顯式地使用sara對象(因為它被作為參數(shù)傳遞),來計算總和,并返回這個值

運算符重載的禁區(qū)和規(guī)則:

  1. 為了防止對標準類型進行運算符重載,C++規(guī)定重載運算符的操作對象至少有一個不是標準類型,而是用戶自定義的類型比如不能重載 1+2

    但是可以重載 cow + 2 和 2 + cow // cow 是自定義的對象

  2. 不能改變原運算符的語法規(guī)則, 比如不能把雙目運算符重載為單目運算

  3. 不能修改運算符的優(yōu)先級。因此,如果將加號運算符重載成將兩個類相加,則新的運算符與原來的加號具有相同的優(yōu)先級。

  4. 不能創(chuàng)建新運算符。例如,不能定義operator **( )函數(shù)來表示求冪。

  5. 不能對以下這四種運算符,使用友元函數(shù)進行重載

    = 賦值運算符,()函數(shù)調(diào)用運算符,[ ]下標運算符,->通過指針訪問類成員

    大多數(shù)運算符都可以通過成員或非成員函數(shù)(一般都是友元函數(shù))進行重載,但這四個運算符只能通過成員函數(shù)進行重載。

    重載的運算符(有些例外情況)不必是成員函數(shù)

  6. 不能對禁止重載的運算符進行重載

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

Cow.h
#pragma once

class Pork;
class Goat;

class Cow
{
public:
    Cow(int weight);
    
    
    Pork operator+(const Cow& cow) ; //同類型進行運算,很頻繁
    // 1、參數(shù)此時定義為引用類型,更合適,避免拷貝
    // 2、實際上,這里的參數(shù)前面還有一個參數(shù)不用指定,默認是本對象
    
    Pork operator+(const Goat& goat) ; //不同類型進行運算,比較少見
private:
    int weight = 0;
};


Cow.cpp 
#include "Cow.h"
#include "Pork.h"
#include "Goat.h"
Cow::Cow(int weight)
{
	this->weight = weight;
}

// 規(guī)則:
// 一斤牛肉:2斤豬肉
// 一斤羊肉:3斤豬肉
Pork Cow::operator+(const Cow &cow)
{
    int tmp = (this->weight + cow.weight) * 2;
    return Pork(tmp);
}
Pork Cow::operator+(const Goat& goat)
{
    // 不能直接訪問goat.weight
    //int tmp = this->weight * 2 + goat.weight * 3;
    int tmp = this->weight * 2 + goat.getWeight() * 3;
    return Pork(tmp);
}

Goat.h 
#pragma once
class Goat
{
public:
    Goat(int weight);
    int getWeight(void) const;
private:
	int weight = 0;
};   

Goat.cpp   
#include "Goat.h"
Goat::Goat(int weight) {
	this->weight = weight;
}
int Goat::getWeight(void) const
{
	return weight;
}

Pork.h
#pragma once
#include <iostream>
class Pork
{
public:
	Pork(int weight);
	std::string description(void);
private:
	int weight = 0;
};

Pork.cpp
#include "Pork.h"
#include <sstream>
Pork::Pork(int weight)
{
	this->weight = weight;
}
std::string Pork::description(void)
{
    std::stringstream ret;
    ret << weight << "斤豬肉";
    return ret.str();
}

main.cpp
#include <iostream>
#include "Pork.h"
#include "Cow.h"
#include "Goat.h"
int main(void) {
    Cow c1(100);
    Cow c2(200);
    // 調(diào)用c1.operator+(c2);
    // 相當于:Pork p = c1.operator+(c2);
    Pork p = c1 + c2;
    std::cout << p.description() << std::endl;
    
    Goat g1(100);
    p = c1 + g1;
    std::cout << p.description() << std::endl;
    
    system("pause");
    return 0;
}

??2.使用非成員函數(shù)【友元函數(shù)】重載運算符

Cow.h
#pragma once
class Pork;
class Goat;

class Cow
{
public:
    Cow(int weight);
    // 有友元函數(shù)實現(xiàn)運算符重載
    friend Pork operator+(const Cow& cow1, const Cow& cow2);
    friend Pork operator+(const Cow& cow1, const Goat& goat); //這里就要加上第一個參數(shù)
private:
	int weight = 0;
};

main.cpp
Pork operator+(const Cow &cow1, const Cow &cow2)
{
    int tmp = (cow1.weight + cow2.weight) * 2;
    return Pork(tmp);
}
Pork operator+(const Cow& cow1, const Goat& goat)
{
    int tmp = cow1.weight * 2 + goat.getWeight() * 3;
    return Pork(tmp);
}

為何需要友元函數(shù):

A= B*2.75 ;
//將被轉(zhuǎn)換為下面的成員函數(shù)調(diào)用:
A= B.operator*(2.75;

但下面的語句又如何呢?
A= 2.75 * B;//cannot correspond to a member function

從概念上說,2.75 * B應與B *2.75相同,但第一個表達式不對應于成員函數(shù),因為2.75不是一種類型的對象。記住,左側(cè)的操作數(shù)應是調(diào)用對象,但2.75不是對象。因此,編譯器不能使用成員函數(shù)調(diào)用來替換該表達式。

可以使用非成員函數(shù)來解決(記住,大多數(shù)運算符都可以通過成員或非成員函數(shù)來重載)非成員函數(shù)不是由對象調(diào)用的,它使用的所有值(包括對象)都是顯式參數(shù),如此:

A= 2.75 * B;
//與下面的非成員函數(shù)調(diào)用匹配:
A=operator* (2.75,B);

該函數(shù)的原型如下:

Time operator* (double m,const Time & t) ;

對于非成員重載運算符函數(shù)來說,運算符表達式左邊的操作數(shù)對應于運算符函數(shù)的第一個參數(shù),運算符表達式右邊的操作數(shù)對應于運算符函數(shù)的第二個參數(shù)。而原來的成員函數(shù)則按相反的順序處理操作數(shù)

使用非成員函數(shù)可以按所需的順序獲得操作數(shù)(先是double,然后是Time),但引發(fā)了一個新問題:非成員函數(shù)不能直接訪問類的私有數(shù)據(jù),至少常規(guī)非成員函數(shù)不能訪問。然而,有一類特殊的非成員函數(shù)可以訪問類的私有成員,它們被稱為友元函數(shù)。

實際上,按下面的方式對定義進行修改(交換乘法操作數(shù)的順序),可以將這個友元函數(shù)編寫為非友元函數(shù):

Time operator* (double m,const Time & t){
	return t *m; // use t.operator* (m)
}

兩種方式的區(qū)別

使用成員函數(shù)來實現(xiàn)運算符重載時,少寫一個參數(shù),因為第一個參數(shù)就是 this 指針。對于成員函數(shù)版本來說,一個操作數(shù)通過this指針隱式地傳遞,另一個操作數(shù)作為函數(shù)參數(shù)顯式地傳遞;對于友元版本來說,兩個操作數(shù)都作為參數(shù)來傳遞。

兩種方式的選擇:

  1. 一般情況下,單目運算符重載,使用成員函數(shù)進行重載更方便(不用寫參數(shù))

  2. 一般情況下,雙目運算符重載,使用友元函數(shù)更直觀

方便實現(xiàn) a+b 和 b+a 相同的效果,成員函數(shù)方式無法實現(xiàn)。

例如:

100 + cow; 只能通過友元函數(shù)來實現(xiàn)

cow +100; 友元函數(shù)和成員函數(shù)都可以實現(xiàn)

特殊情況:

(1) = () [ ] -> 不能重載為類的友元函數(shù)!?。。ǚ駝t可能和 C++的其他規(guī)則矛盾),只能使用成員函數(shù)形式進行重載。

(2)如果運算符的第一個操作數(shù)要求使用隱式類型轉(zhuǎn)換,則必須為友元函數(shù)(成員函數(shù)方式的第一個參數(shù)是 this 指針)

注意:

同一個運算符重載, 不能同時使用兩種方式來重載,會導致編譯器不知道選擇哪一個(二義性)

?(三)運算符重載實例

1.重載賦值運算符=

就是前面講的賦值構造函數(shù)

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    Boy& operator=(const Boy& boy);
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑馬值,潛力系數(shù)
    unsigned int id; // 編號
    static int LAST_ID;
};

Boy.cpp

#include "boy.h"
#include <string.h>
#include <sstream>

int Boy::LAST_ID = 0; //初始值是0

// 注意返回類型 和參數(shù)類型
Boy& Boy::operator=(const Boy& boy)
{
    if (name) {
    delete name; //釋放原來的內(nèi)存
    }
    name = new char[strlen(boy.name) + 1]; //分配新的內(nèi)存
    strcpy_s(name, strlen(boy.name)+1, boy.name);
    
    this->age = boy.age;
    this->salary = boy.salary;
    this->darkHorse = boy.darkHorse;
    //this->id = boy.id; //根據(jù)需求來確定是否要拷貝id
    return *this;
}
...

main.cpp

#include <iostream>
#include "boy.h"

int main(void) {
    Boy boy1("Rock", 38, 58000, 10);
    Boy boy2, boy3;
    
    std::cout << boy1.description() << std::endl;
    std::cout << boy2.description() << std::endl;
    std::cout << boy3.description() << std::endl;
    
    boy3 = boy2 = boy1;
    std::cout << boy2.description() << std::endl;
    std::cout << boy3.description() << std::endl;
    system("pause");
    return 0;
}

注意:

注意賦值運算符重載的返回類型和參數(shù)類型。

  • 返回引用類型,便于連續(xù)賦值
  • 參數(shù)使用應用類型, 可以省去一次拷貝
  • 參數(shù)使用 const, 便于保護實參不被破壞

賦值運算符的重載,應該使用這種方式:

Boy& operator=(const Boy &boy);

就是:參數(shù)要使用引用!

如果定義成:

Boy& operator=(const Boy *boy);

將會沒有效果,編譯器不會識別為賦值運算符的重載,

也就是:boy2 = boy1 時不會調(diào)用這個函數(shù)

如果定義:

Boy& operator=(const Boy boy);

有效果,但是在調(diào)用時,會執(zhí)行參數(shù)的傳遞:

比如:boy2 = boy1;

就會執(zhí)行: boy2.operator=(boy1);

就會執(zhí)行: const Boy boy = boy1;

就會執(zhí)行: Boy 類的賦值構造函數(shù)

有兩個影響:

1) 浪費性能

2) 如果沒有自定義的拷貝構造函數(shù),而且這個類又有指針成員時,就會調(diào)用自動生成的拷貝構

造函數(shù),導致淺拷貝

如果析構函數(shù)中,對這個指針指向的內(nèi)存做了釋放,那就導致數(shù)據(jù)損壞或崩潰!

小結(jié):

1)賦值運算符的重載,一定要使用引用參數(shù)

2)如果一個類有指針成員,而且使用了動態(tài)內(nèi)存分配,那么一定要定義自己的拷貝構造函數(shù)【要使

用深拷貝】,避免調(diào)用自動生成的拷貝構造函數(shù)

因為自動生成的拷貝構造函數(shù),是淺拷貝

??2.重載關系運算符>、<、==

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    
    Boy& operator=(const Boy& boy);
    
    bool operator>(const Boy& boy);
    bool operator<(const Boy& boy);
    bool operator==(const Boy& boy);
    
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑馬值,潛力系數(shù)
    unsigned int id; // 編號
    static int LAST_ID;
};

Boy.cpp

bool Boy::operator>(const Boy& boy)
{
    // 設置比較規(guī)則:
    // 薪資 * 黑馬系數(shù) + (100-年齡)*100
    if (power() > boy.power()) {
    	return true;
    }
    else {
    	return false;
    }
}

bool Boy::operator<(const Boy& boy)
{
    if (power() < boy.power()) {
    	return true;
    }
    else {
    	return false;
        }
}

bool Boy::operator==(const Boy& boy)
{
    if (power() == boy.power()) {
    	return true;
    }
    else {
    	return false;
    }
}

main.cpp

int main(void) {
    Boy boy1("Rock", 38, 58000, 5);
    Boy boy2("Jack", 25, 50000, 10);
    
    if (boy1 > boy2) {
    	std::cout << "選擇boy1" << std::endl;
    }
    else if (boy1 == boy2) {
    	std::cout << "難以選擇" << std::endl;
    }
    else {
    	std::cout << "選擇boy2" << std::endl;
    }
    
    system("pause");
    return 0;
}

??3.重載下標運算符[]

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    
    Boy& operator=(const Boy& boy);
    
    bool operator>(const Boy& boy);
    bool operator<(const Boy& boy);
    bool operator==(const Boy& boy);
    
    int operator[](std::string index);
	int operator[](int index);
    
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑馬值,潛力系數(shù)
    unsigned int id; // 編號
    static int LAST_ID;
};

Boy.cpp

int Boy::operator[](std::string index)
{
    if (index == "age") {
    	return age;
    }
    else if (index == "salary") {
    	return salary;
    }
    else if (index == "darkHorse") {
    	return darkHorse;
    }
    else if (index == "power") {
    	return power();
    }
    else {
    	return -1;
    }
}

int Boy::operator[](int index)
{
    if (index == 0) {
    	return age;
    }
    else if (index == 1) {
    	return salary;
    }
    else if (index == 2) {
    	return darkHorse;
    }
    else if (index == 3) {
    	return power();
    }
    else {
    	return -1;
    }
}

main.cpp

int main(void) {
    Boy boy1("Rock", 38, 58000, 5);
    Boy boy2("Jack", 25, 50000, 10);
    
    std::cout << "age:" << boy1["age"] << std::endl;
    std::cout << "salary:" << boy1["salary"] << std::endl;
    std::cout << "darkHorse:" << boy1["darkHorse"] << std::endl;
    std::cout << "power:" << boy1["power"] << std::endl;
    
    std::cout << "[0]:" << boy1[0] << std::endl;
    std::cout << "[1]:" << boy1[1] << std::endl;
    std::cout << "[2]:" << boy1[2] << std::endl;
    std::cout << "[3]:" << boy1[3] << std::endl;
    
    system("pause");
    return 0;
}

??4.重載<<和>>運算符

(1)重載<<運算符

一個很有用的類特性是,可以對<<運算符進行重載,使之能與cout一起來顯示對象的內(nèi)容。

前面我們都是采用一個成員函數(shù)description()來對對象進行描述,現(xiàn)在我們通過重載使用以下操作:

cout << Father;

實際上,它已經(jīng)被重載很多次了。最初,<<運算符是C和C++的位運算符,將值中的位左移,ostream類對該運算符進行了重載,將其轉(zhuǎn)換為一個輸出工具。前面講過,cout是一個ostream對象,它是智能的,能夠識別所有的C++基本類型。這是因為對于每種基本類型,ostream類聲明中都包含了相應的重載的operator<<( )定義。因此,要使cout能夠識別Time對象,一種方法是將一個新的函數(shù)運算符定義添加到ostream類聲明中。但修改iostream文件是個危險的主意,這樣做會在標準接口上浪費時間。相反,通過類聲明來讓類知道如何使用cout。

①<<的第一種重載版本

要使類知道如何使用cout,必須使用友元函數(shù)。這是什么原因呢?因為下面這樣的語句使用兩個對象,其中第一個是ostream類對象(cout):

cout << Father;

如果使用一個Father成員函數(shù)來重載<<,F(xiàn)ather對象將是第一個操作數(shù),就像使用成員函數(shù)重載*運算符那樣。這意味著必須這樣使用<<:

Father << cout;

這樣會令人迷惑。但通過使用友元函數(shù),可以像下面這樣重載運算符:

void operator<< (ostream & os,const Time & t){
	os << t.hours << " hours, " << t.minutes << " minutes" ;
}

新的Time類聲明使operatro<<( )函數(shù)成為Time類的一個友元函數(shù)。但該函數(shù)不是ostream類的友元(盡管對ostream類并無害處)。operator<<( )函數(shù)接受一個ostream參數(shù)和一個Time參數(shù),因此表面看來它必須同時是這兩個類的友元。然而,看看函數(shù)代碼就會發(fā)現(xiàn),盡管該函數(shù)訪問了Time對象的各個成員,但從始至終都將ostream對象作為一個整體使用。因為operator<<( )直接訪問Time對象的私有成員,所以它必須是Time類的友元。但由于它并不直接訪問ostream對象的私有成員,所以并不一定必須是ostream類的友元。這很好,因為這就意味著不必修訂ostream的定義。

新的operator<<( )定義使用ostream引用os作為它的第一個參數(shù)。通常情況下,os引用cout對象,如表達式cout << trip所示。但也可以將這個運算符用于其他ostream對象,在這種情況下,os將引用相應的對象:另一個ostream對象是cerr,它將輸出發(fā)送到標準輸出流——默認為顯示器,但在UNIX、Linux和Windows命令行環(huán)境中,可將標準錯誤流重定向到文件。另外,第6章介紹的ofstream對象可用于將輸出寫入到文件中。通過繼承(參見第13章),ofstream對象可以使用ostream的方法。這樣,便可以用operator<<( )定義來將Time的數(shù)據(jù)寫入到文件和屏幕上,為此只需傳遞一個經(jīng)過適當初始化的ofstream對象(而不是cout對象)。
調(diào)用cout << trip應使用cout對象本身,而不是它的拷貝,因此該函數(shù)按引用(而不是按值)來傳遞該對象。這樣,表達式cout << trip將導致os成為cout的一個別名;而表達式cerr << trip將導致os成為cerr的一個別名。Time對象可以按值或按引用來傳遞,因為這兩種形式都使函數(shù)能夠使用對象的值。按引用傳遞使用的內(nèi)存和時間都比按值傳遞少。

②<<的第二種重載版本

前面介紹的實現(xiàn)存在一個問題。不允許像通常那樣將重新定義的<<運算符與cout一起使用:

cout <c "Trip time: " <c trip <c "( Tuesday)|n" ; // can't do

cout << x < y;
//C++從左至右讀取輸出語句,意味著它等同于:
(cout << x) << y;

正如iosream中定義的那樣,<<運算符要求左邊是一個ostream對象。顯然,因為cout是ostream對象,所以表達式cout << x滿足這種要求。然而,因為表達式cout << x位于<< y的左側(cè),所以輸出語句也要求該表達式是一個ostream類型的對象。因此,ostream類將operator<<( )函數(shù)實現(xiàn)為返回一個指向ostream對象的引用。具體地說,它返回一個指向調(diào)用對象(這里是cout)的引用。因此,表達式(cout << x)本身就是ostream對象cout,從而可以位于<<運算符的左側(cè)。

可以對友元函數(shù)采用相同的方法。只要修改operator<<( )函數(shù),讓它返回ostream對象的引用即可:

ostream & operator<<(ostream & os,const Time & t){
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os;
}

這個operator<<( )版本還可用于將輸出寫入到文件中:

#include <fstream>
...
ofstream fout;
fout.open ( "savetine.txt ");
Time trip (12,40);
fout << trip;

其中最后一條語句將被轉(zhuǎn)換為這樣:

operator<<(fout, trip);
(2)重載>>運算符

和<<一樣,使用成員函數(shù)的方式不方便,同樣使用友元函數(shù)

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    
    Boy& operator=(const Boy& boy);
    
    bool operator>(const Boy& boy);
    bool operator<(const Boy& boy);
    bool operator==(const Boy& boy);
    
    int operator[](std::string index);
	int operator[](int index);
    
    // 該方式不適合
    //ostream& operator<<(ostream& os) const;
    
    friend ostream& operator<<(ostream& os, const Boy& boy);
    friend istream& operator>>(istream& is, Boy& boy);
    
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑馬值,潛力系數(shù)
    unsigned int id; // 編號
    static int LAST_ID;
};

Boy.cpp

//ostream& Boy::operator<<(ostream& os) const
//{
// 		os << "ID:" << id << "\t姓名:" << name << "\t年齡:" << age << "\t薪資:"
// 		   << salary << "\t黑馬系數(shù):" << darkHorse;
// 		return os;
//}

ostream& operator<<(ostream& os, const Boy& boy) {
    os << "ID:" << boy.id << "\t姓名:" << boy.name << "\t年齡:" << boy.age << "\t薪資:"
       << boy.salary << "\t黑馬系數(shù):" << boy.darkHorse;
    return os;
}

istream& operator>>(istream& is, Boy& boy)
{
    string name2;
    is >> name2 >> boy.age >> boy.salary >> boy.darkHorse;
    boy.name = (char*)malloc((name2.length()+1) * sizeof(char));
    strcpy_s(boy.name, name2.length() + 1, name2.c_str());
    return is;
}

main.cpp

int main(void) {
   Boy boy1("Rock", 38, 58000, 5);
    Boy boy2("Jack", 25, 50000, 10);
    
    cout << boy1 << endl;
    cin >> boy1;
    cout << boy1;
    
    system("pause");
    return 0;
}

??5.重載類型轉(zhuǎn)換運算符函數(shù)

(1)類的自動轉(zhuǎn)換和強制類型轉(zhuǎn)換(普通類型 -> 類類型)

可以將類定義成與基本類型或另一個類相關,使得從一種類型轉(zhuǎn)換為另一種類型是有意義的。在這種情況下,程序員可以指示C++如何自動進行轉(zhuǎn)換,或通過強制類型轉(zhuǎn)換來完成。

可以提供一些將整數(shù)或浮點值轉(zhuǎn)換為對象的方法,其實我們已經(jīng)這樣做了,在C++中,接受一個參數(shù)的構造函數(shù)為將類型與該參數(shù)相同的值轉(zhuǎn)換為類提供了藍圖,例如,下面的構造函數(shù)用于將int類型的值轉(zhuǎn)換為Boy類型:

Boy(int salary);
 
//也就是說,可以編寫這樣的代碼:
Boy chenQi; 
chenQi = 19; 

程序?qū)⑹褂脴嬙旌瘮?shù)Boy(int salary)來創(chuàng)建一個臨時的Boy對象,并將19作為初始化值。隨后,采用逐成員賦值方式將該臨時對象的內(nèi)容復制到chenQi中。這一過程稱為隱式轉(zhuǎn)換,因為它是自動進行的,而不需要顯式強制類型轉(zhuǎn)換。

只有接受一個參數(shù)的構造函數(shù)才能作為轉(zhuǎn)換函數(shù)。但是如果有兩個參數(shù)或者更多,可以給第一個后面的參數(shù)都提供默認值,它便可作為轉(zhuǎn)換函數(shù)

要達到類似于:stone = 1000,從整數(shù)轉(zhuǎn)換為類類型,其實就是要重載構造函數(shù),就需要重載類型轉(zhuǎn)換運算符

class Boy
{
public:
    //Boy(const char* name = NULL, int age = 0, int salary = 0, int darkHorse = 0);
    Boy(const char* name, int age, int salary, int darkHorse);
    ~Boy();
    
    Boy(int salary);
    Boy(const char* name);
    ...
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑馬值,潛力系數(shù)
    unsigned int id; // 編號
    static int LAST_ID;
    int power() const; //綜合能力值
};

Boy::Boy(int salary)
{
    const char *defaultName = "未命名";
    name = new char[strlen(defaultName) + 1];
    strcpy_s(name, strlen(defaultName) + 1, defaultName);
    
    age = 0;
    this->salary = salary;
    darkHorse = 0;
    this->id = ++LAST_ID;
}

Boy::Boy(const char* name) {
    this->name = new char[strlen(name) + 1];
    strcpy_s(this->name, strlen(name) + 1, name);
    
    age = 0;
    this->salary = 0;
    this->sdarkHorse = 0;
    this->id = ++LAST_ID;
}

int main()
{
    Boy boy1 = 10000;
    Boy boy2 = "Rock";
    
    cout << boy1 << endl;
    cout << boy2 << endl;
    
    boy1 = 20000; //boy1 = Boy(20000);
    cout << boy1 << endl;
    
    return 0;
}

只接受一個參數(shù)的構造函數(shù)定義了從參數(shù)類型到類類型的轉(zhuǎn)換。如果使用關鍵字explicit限定了這種構造函數(shù),則它只能用于顯示轉(zhuǎn)換,否則也可以用于隱式轉(zhuǎn)換。

1.編譯器在什么時候?qū)⑹褂肂oy(int salary)函數(shù)呢?如果在聲明中使用了關鍵字explicit,則Boy(int salary)將只用于顯式強制類型轉(zhuǎn)換,否則還可以用于下面的隱式轉(zhuǎn)換。

  • 將Boy對象初始化為int值時。
  • 將int值賦給Boy對象時。
  • 將int值傳遞給接受Boy參數(shù)的函數(shù)時。
  • 返回值被聲明為Boy的函數(shù)試圖返回int值時。

在上述任意一種情況下,使用可轉(zhuǎn)換為int類型的內(nèi)置類型時。

2.函數(shù)原型化提供的參數(shù)匹配過程,允許使用Boy(int)構造函數(shù)來轉(zhuǎn)換其他數(shù)值類型。也就是說,下面兩條語句都首先將double轉(zhuǎn)換為int,然后使用Boy(int)構造函數(shù)。

Boy chenQi(19.3); 
chenQi = 20.5

然而,當且僅當轉(zhuǎn)換不存在二義性時,才會進行這種二步轉(zhuǎn)換。也就是說,如果這個類還定義了構造函數(shù)Boy(double),則編譯器將拒絕這些語句,可能指出:double可被轉(zhuǎn)換為double或int,因此調(diào)用存在二義性。

(2)類類型 -> 普通類型

普通類型到類類型是使用構造函數(shù),但是構造函數(shù)只用于從某種類型到類類型的轉(zhuǎn)換。要進行相反的轉(zhuǎn)換,必須使用特殊的C++運算符函數(shù)——轉(zhuǎn)換函數(shù)。轉(zhuǎn)換函數(shù)是用戶定義的強制類型轉(zhuǎn)換,可以像使用強制類型轉(zhuǎn)換那樣使用它們。例如:

Boy chenQi(19); 
int test1 = int(chenQi);       //syntax #1
int test2 = (int)chenQi;       //syntax #2

//也可以讓編譯器來決定如何做:
Boy chenQi(19); 
int test3 = chenQi;
//編譯器發(fā)現(xiàn),右側(cè)是chenQi類型,而左側(cè)是int類型,因此它將查看程序員是否定義了與此匹配的轉(zhuǎn)換函數(shù)。(如果沒有找到這樣的定義,編譯器將生成錯誤消息,指出無法將chenQi賦給int。)

轉(zhuǎn)換函數(shù)的形式:

operator typeName();
  • 轉(zhuǎn)換函數(shù)必須是類方法;意味著它需要通過類對象來調(diào)用,從而告知函數(shù)要轉(zhuǎn)換的值。因此,函數(shù)不需要參數(shù)。
  • 轉(zhuǎn)換函數(shù)不能指定返回類型;
  • 轉(zhuǎn)換函數(shù)不能有參數(shù)。
class Boy
{
public:
    //Boy(const char* name = NULL, int age = 0, int salary = 0, int darkHorse = 0);
    Boy(const char* name, int age, int salary, int darkHorse);
    ~Boy();
    
	// 特殊的運算符重載:類型轉(zhuǎn)換函數(shù),不需要寫返回類型
    operator int() const;
    operator char* () const;	
    ...
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑馬值,潛力系數(shù)
    unsigned int id; // 編號
    static int LAST_ID;
    int power() const; //綜合能力值
};

Boy::operator int() const
{
	return power();
}

Boy::operator char* () const
{
	return name;
}

int main()
{
    Boy boy1("Rock", 28, 10000, 5);
    Boy boy2("Rock");
    
    int power = boy1;
    char* name = boy2;
    
    cout << power << endl;
    cout << name << endl;

    system("pause");
    return 0;
}

1.當類定義了兩種或更多的轉(zhuǎn)換時,仍可以用顯式強制類型轉(zhuǎn)換來指出要使用哪個轉(zhuǎn)換函數(shù)??梢允褂孟旅嫒魏我环N強制類型轉(zhuǎn)換表示法:

int power = (int)boy1;
char* name = (char*)boy2;

2.在C++98中,關鍵字explicit不能用于轉(zhuǎn)換函數(shù),但C++11消除了這種限制。因此,在C++11中,可將轉(zhuǎn)換運算符聲明為顯式的

3.用一個功能相同的非轉(zhuǎn)換函數(shù)替換該轉(zhuǎn)換函數(shù)即可,但僅在被顯式地調(diào)用時,該函數(shù)才會執(zhí)行。

int Boy::Boy_to_Int() const
{
	return power();
}

int test4 = Boy.Boy_to_Int();
(3)類類型A -> 類類型B

調(diào)用對應的只有一個參數(shù)【參數(shù)的類型就是類類型 A】的構造函數(shù)

也可以使用類型轉(zhuǎn)換函數(shù),但是使用對應的構造函數(shù)更合適。

實例:

把 Boy 類型,轉(zhuǎn)換為 Man 類型

Man.h

class Boy;

class Man
{
public:
    Man(const char *name, int age, int salary);
    Man(const Boy& boy);
    ~Man();
    friend ostream& operator<<(ostream &os, const Man& man);
private:
    char* name;
    int age;
    int salary;
};

ostream& operator<<(ostream &os, const Man& man)

Man.cpp

Man::Man(const Boy& boy)
{
    int len = strlen((char*)boy) + 1;
    name = new char[len];
    strcpy_s(name, len, (char*)boy);
    age = boy[AGE];
    salary = boy[SALARY];
}

main.cpp

int main()
{
    Boy boy("Rock", 28, 10000, 5);
    Man man = boy;
    
    cout << boy << endl;
    cout << man << endl;
    
    system("pause");
    return 0;
}
(4)explicit關鍵字

將構造函數(shù)用作自動類型轉(zhuǎn)換函數(shù)似乎是一項不錯的特性。然而,當程序員擁有更豐富的C++經(jīng)驗時,將發(fā)現(xiàn)這種自動特性并非總是合乎需要的,因為這會導致意外的類型轉(zhuǎn)換。因此,C++新增了關鍵字explicit,用于關閉這種自動特性。也就是說,可以這樣聲明構造函數(shù):

explicit Boy(int salary);

作用是表明該構造函數(shù)是顯示的, 而非隱式的.不能進行隱式轉(zhuǎn)換,但仍然允許顯式轉(zhuǎn)換,即顯式強制類型轉(zhuǎn)換! 跟它相對應的另一個關鍵字是 implicit, 意思是隱藏的,類構造函數(shù)默認情況下即聲明為 implicit(隱式).

//示例1:
Boy chenQi; 
chenQi = 19;       //not valid if Boy(int)is declared as explicit
chenQi = Boy(19);  //ok,an explicit conversion
chenQi = (Boy)19;  //ok, old form for explicit typecast

//示例2:
#include <iostream>
#include <string>

using namespace std;
class student {
public:
    student(int _age)
    {
        age = _age;
        cout << "age=" << age << endl;
    }
    
    student(int _age, const string _name)
    {
        age = _age;
        name = _name;
        cout << "age=" << age << "; name=" << name << endl;
    }
    
    ~student()
    {
    }
    	
    int getAge()
    {
    	return age;
    }
    
    string getName() {
    
        return name;
    }
private:
	int age;
    string name;
};

int main(void) {
    student xiaoM(18); //顯示構造
    student xiaoW = 18; //隱式構造
    //student xiaoHua(19, "小花"); //顯示構造
    //student xiaoMei = { 18, "小美" }; //隱式構造 初始化參數(shù)列表,C++11 前編譯不能通過,C++11 新增特性
    system("pause");
    return 0;
}

??三、多態(tài)

在C++中有兩種多態(tài)性,一種是靜態(tài)的多態(tài)、一種是動態(tài)的多態(tài);

靜態(tài)的多態(tài):函數(shù)重載,看起來調(diào)用同一個函數(shù)卻有不同的行為。靜態(tài):原理是編譯時實現(xiàn)。

動態(tài)的多態(tài):一個父類的引用或指針去調(diào)用同一個函數(shù),傳遞不同的對象,會調(diào)用不同的函數(shù)。動態(tài):原理是運行時實現(xiàn)。

?(一)多態(tài)的實現(xiàn):虛函數(shù)

??1.基礎

#include <iostream>
using namespace std;

class Father {
public:
    void play() {
    	cout << "到 KTV 唱歌..." << endl;
    }
};

class Son :public Father {
public:
    void play() {
    	cout << "一起打王者吧!" << endl;
    }
};
	
void party(Father **men, int n) {
    for (int i = 0; i<n; i++) {
    	men[i]->play();
    }
}
int main(void) {
    Father father;
    Son son1, son2;
    
    //這里父類指針可以指向子類型對象,F(xiàn)ather* P,P=&son1,p->play()因為指針是父類的,所以這里還是會調(diào)用父類的play()方法
    Father* men[] = { &father, &son1, &son2 };
    
    
    party(men, sizeof(men) / sizeof(men[0]));
    system("pause");
    return 0;
}

解決方案:通過虛函數(shù),實現(xiàn)多態(tài)

class Father {
public:
    virtual void play() {
    	cout << "到 KTV 唱歌..." << endl;
    }
};

class Son :public Father {
public:
    //子類中可以寫也可以不寫
    virtual void play() {
    	cout << "一起打王者吧!" << endl;
    }
};
  • 多態(tài)構成條件:

    在繼承中要構成多態(tài)還有兩個條件:

    1. 必須通過基類的指針或者引用調(diào)用虛函數(shù)。

    2. 被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對基類的虛函數(shù)進行重寫。

  • 多態(tài)的本質(zhì):

    使用virtual指明虛函數(shù),如果方法是通過引用或指針而不是對象調(diào)用的,它將確定使用哪一種方法。如果沒有使用關鍵字virtual,程序?qū)⒏鶕?jù)引用類型或指針類型選擇方法;如果使用了virtual,程序?qū)⒏鶕?jù)引用或指針指向的對象的類型來選擇方法。

    程序執(zhí)行時,父類指針指向父類對象,或子類對象時,在形式上是無法分辨的!只有通過多態(tài)機制,才能執(zhí)行真正對應的方法。

  • 虛函數(shù):

    1. 虛函數(shù)的定義:

    在函數(shù)的返回類型之前使用 virtual,只在成員函數(shù)的聲明中添加 virtual, 在成員函數(shù)的實現(xiàn)中不要加 virtual

    1. 虛函數(shù)的繼承:

    如果某個成員函數(shù)被聲明為虛函數(shù),那么它的子類【派生類】,以及子類的子類中,所繼承的這個成員函數(shù),也自動是虛函數(shù)。經(jīng)常在基類中將派生類會重新定義的方法聲明為虛方法,方法在基類中被聲明為虛的后,它在派生類中將自動成為虛方法。但建議子類中也寫上virtual

  • 虛析構函數(shù):

    如果基類的析構函數(shù)不使用虛函數(shù)的形式:派生類開始從基類繼承,基類的指針指向派生類的對象時,delete基類的指針時,只會調(diào)用基類的析構函數(shù),不會調(diào)用派生類的析構函數(shù)。

    為了避免內(nèi)存泄漏,而且是當子類中會有指針成員變量時才會使用到。即虛析構函數(shù)使得在刪除指向子類對象的基類指針時,會先調(diào)用派生類的析構函數(shù),再自動調(diào)用基類中的析構函數(shù)。

    基類聲明了一個虛析構函數(shù)。這樣做是為了確保釋放派生對象時,按正確的順序調(diào)用析構函數(shù)

    #include <iostream>
    #include <Windows.h>
    #include <string.h>
    
    using namespace std;
    class Father {
    public:
    	Father(const char* addr ="中國"){
            cout << "執(zhí)行了 Father 的構造函數(shù)" << endl;
            int len = strlen(addr) + 1;
            this->addr = new char[len];
            strcpy_s(this->addr, len, addr);
    	}
    	
        // 把 Father 類的析構函數(shù)定義為 virtual 函數(shù)時,
        // 如果對 Father 類的指針使用 delete 操作時,
        // 就會對該指針使用“動態(tài)析構”:
        // 如果這個指針,指向的是子類對象,
        // 那么會先調(diào)用該子類的析構函數(shù),再調(diào)用自己類的析構函數(shù)
        virtual ~Father(){
        	cout << "執(zhí)行了 Father 的析構函數(shù)" << endl;
            if (addr) {
                delete addr;
                addr = NULL;
            }
        }
    private:
    	char* addr;
    };
    
    class Son :public Father {
    public:
        Son(const char *game="吃雞", const char *addr="中國")
            :Father(addr){
            cout << "執(zhí)行了 Son 的構造函數(shù)" << endl;
            int len = strlen(game) + 1;
            this->game = new char[len];
            strcpy_s(this->game, len, game);
        }
        ~Son(){
            cout << "執(zhí)行了 Son 的析構函數(shù)" << endl;
            if (game) {
                delete game;
                game = NULL;
            }
        }
    private:
    	char* game;
    };
    
    int main(void) {
        cout << "----- case 1 -----" << endl;
        Father* father = new Father();
        delete father;
        
        cout << "----- case 2 -----" << endl;
        Son* son = new Son();
        delete son;
        
        cout << "----- case 3 -----" << endl;
        father = new Son();
        delete father;
        
        system("pause");
        return 0;
    }
    

如果要在派生類中重新定義基類的方法,通常應將基類方法聲明為虛的。這樣,程序?qū)⒏鶕?jù)對象類型而不是引用或指針的類型來選擇方法版本。為基類聲明一個虛析構函數(shù)也是一種慣例。

注意事項:

  • 構造函數(shù)不能是虛函數(shù)。創(chuàng)建派生類對象時,將調(diào)用派生類的構造函數(shù),而不是基類的構造函數(shù),然后,派生類的構造函數(shù)將使用基類的一個構造函數(shù),這種順序不同于繼承機制。因此,派生類不繼承基類的構造函數(shù),所以將類構造函數(shù)聲明為虛的沒什么意義。

  • 析構函數(shù)應當是虛函數(shù),除非類不用做基類。例如,假設Employee是基類,Singer是派生類,并添加一個char *成員,該成員指向由new分配的內(nèi)存。當Singer對象過期時,必須調(diào)用~Singer( )析構函數(shù)來釋放內(nèi)存:

    Employee * pe = new Singer; // legal because Employee is base for Singer
    ...
    delete pe;                  //~Employee() or ~singer ()
    
  • 友元不能是虛函數(shù),因為友元不是類成員,而只有成員才能是虛函數(shù)。

  • 如果派生類沒有重新定義函數(shù),將使用該函數(shù)的基類版本。如果派生類位于派生鏈中,則將使用最新的虛函數(shù)版本,

??2.虛函數(shù)表

虛函數(shù)表指針:

class Father {
public:
    virtual void func1() { cout << "Father::func1" << endl; }
private: 
	int m_b = 1;
	char m_ch = 'A';
};

int main(void) {
	
	Father father;
	cout << sizeof(father) << endl;
	
    return 0;
}

在C語言講結(jié)構時,講到了結(jié)構體對齊規(guī)則,對于類來說也有這樣一個規(guī)則,我們統(tǒng)一叫作內(nèi)存對齊,而在類中還需要注意一個點,除了各種數(shù)據(jù)要占內(nèi)存外,當我們使用虛函數(shù)時,會產(chǎn)生一個虛函數(shù)表指針_vfptr占一個指針的內(nèi)存的。即8個字節(jié)(64位系統(tǒng))

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

所以這里father類應該占4*4=16個字節(jié)

(1)單個類的虛函數(shù)表

對象中的這個指針叫做虛函數(shù)表指針,簡稱虛表指針,虛表指針指向一個虛函數(shù)表,簡稱虛表,每一個含有虛函數(shù)的類中都有一個虛表指針。那么這個虛表中到底是什么呢?我們通過下面的程序來進行分析:

//case1:類中只有變量
class Base1
{
public:
    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
    
private:
    int base1_1;
    int base1_2;
};

int main() {
    Base1 b1;
    Base1 b2;
}
  • 對象的非虛函數(shù),保存在類的代碼中!對象的內(nèi)存,只存儲虛函數(shù)表,即一個指針和數(shù)據(jù)成員(類的靜態(tài)數(shù)據(jù)成員,保存在數(shù)據(jù)區(qū)中,和對象是分開存儲的)

  • 添加多個虛函數(shù)后,對象的內(nèi)存空間不變,始終存儲一個虛函數(shù)指針!僅虛函數(shù)表中添加條目

  • 虛函數(shù)指針為一個二級指針,指向一個虛函數(shù)表,表中存儲我們的虛函數(shù),同一個類的多個對象,共享同一個虛函數(shù)表:

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

  • 對象內(nèi),首先存儲的是“虛函數(shù)表指針”,又稱“虛表指針”。然后再存儲非靜態(tài)數(shù)據(jù)成員。由此我們可以訪問到整個虛函數(shù)表:

    //通過虛指針訪問虛函數(shù)表并且調(diào)用虛函數(shù)表內(nèi)函數(shù)實現(xiàn)多態(tài),并能夠任意訪問虛函數(shù)
    #include <iostream>
    using namespace std;
    
    class Father {
    public:
        virtual void func1() { cout << "Father::func1" << endl; }
        virtual void func2() { cout << "Father::func2" << endl; }
        virtual void func3() { cout << "Father::func3" << endl; }
        void func4() { cout << "非虛函數(shù):Father::func4" << endl; }
    public: //為了便于測試,特別該用 public
        int x = 100;
        int y = 200;
        static int z;
    };
    
    typedef void (*func_t)(void);
    
    int Father::z = 1;
    
    
    int main(void) {
    
        Father father;
        // 含有虛函數(shù)的對象的內(nèi)存中,最先存儲的就是“虛函數(shù)表”
        cout << "對象地址:" << (int*)&father << endl;
    
        int* vptr = (int*)*(int*)&father;
        cout << "虛函數(shù)表指針 vptr:" << vptr << endl;
    
        cout << "調(diào)用第 1 個虛函數(shù): ";
        ((func_t) * (vptr + 0))();
    
        cout << "調(diào)用第 2 個虛函數(shù):";
        ((func_t) * (vptr + 1))();
    
        cout << "調(diào)用第 3 個虛函數(shù): ";
        ((func_t) * (vptr + 2))();
    
        cout << "第 1 個數(shù)據(jù)成員的地址: " << endl;
        cout << &father.x << endl;
        cout << std::hex << (int)&father + 4 << endl;
        cout << "第 1 個數(shù)據(jù)成員的值:" << endl;
        cout << std::dec << father.x << endl;
        cout << *(int*)((int)&father + 4) << endl;
    
        cout << "第 2 個數(shù)據(jù)成員的地址: " << endl;
        cout << &father.y << endl;
        cout << std::hex << (int)&father + 8 << endl;
        cout << "第 2 個數(shù)據(jù)成員的值:" << endl;
        cout << std::dec << father.y << endl;
        cout << *(int*)((int)&father + 8) << endl;
    
        cout << "sizeof(father)==" << sizeof(father) << endl;
    
        Father father2;
        cout << "father 的虛函數(shù)表:";
        cout << *(int*)(*(int*)&father) << endl;
        cout << "father2 的虛函數(shù)表:";
        cout << *(int*)(*(int*)&father2) << endl;
    
        system("pause");
        return 0;
    }
    

    對于類的每個對象,編譯器都會為其生成一個虛函數(shù)表指針,位于該對象內(nèi)存中的開頭,并指向了虛函數(shù)表的位置。

    ①虛函數(shù)表指針:(int *) &father

    解釋:&father得到對象father的首地址,強制轉(zhuǎn)換為(int *),意為將從&father開始的4個字節(jié)看作一個整體,而&father就是這個4字節(jié)整體的首地址,就是一個指向虛函數(shù)表的指針

    (二級指針,存儲的虛函數(shù)表的地址)

    ②虛函數(shù)表地址:* (int *) &father

    解釋:虛函數(shù)表指針是個指向虛函數(shù)表二級指針,所以再*解引用,就是虛函數(shù)表的地址

    (虛函數(shù)表是個函數(shù)指針數(shù)組)

    ③虛函數(shù)指針(以虛函數(shù)表中的第二個虛函數(shù)指針為例子):

    解釋:(int *)*(int *)&father取到的就是指向第一個虛函數(shù)的指針,那么,我們直接讓這個指針+1,也就是地址移動4個字節(jié),就是第二個虛函數(shù)指針的地址了

    ④虛函數(shù)地址(以虛函數(shù)表的第一個虛函數(shù)指針指向的虛函數(shù)為例子):*(int*) * (int *) &father

    解釋:*(int *)&father就是虛函數(shù)表的地址,然后取前4個字節(jié)作為一個int*指針,這個指針就是指向第一個虛函數(shù)的指針,最后用*解引用即可

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

//通過虛指針訪問虛函數(shù)表并且調(diào)用虛函數(shù)表內(nèi)函數(shù)實現(xiàn)多態(tài),并能夠任意訪問虛函數(shù)
#include <iostream>
using namespace std;

class Father {
public:
 virtual void func1() { cout << "Father::func1" << endl; }
 virtual void func2() { cout << "Father::func2" << endl; }
 virtual void func3() { cout << "Father::func3" << endl; }
 void func4() { cout << "非虛函數(shù):Father::func4" << endl; }
private: 
 int x = 100;
 int y = 200;
 static int z;
};

typedef void (*func_t)(void);

int Father::z = 1;

int main(void) {

 Father father;
 // 含有虛函數(shù)的對象的內(nèi)存中,最先存儲的就是“虛函數(shù)表”
 cout << "對象地址:" << &father << endl << endl;

 cout << "得到虛函數(shù)表指針(二級指針存儲的虛函數(shù)表的地址):" << (int*)&father << endl << endl;

 //前面也可以加一個int * 表示轉(zhuǎn)換成指針顯示,因為是個數(shù)組,其實和下面的第一個函數(shù)指針起始地址是一樣的,
 //形式上自然也一樣的,但是含義上不一樣,和上面的對象地址到得到虛函數(shù)表指針一樣
 cout << "得到虛函數(shù)表地址(虛函數(shù)表是個函數(shù)指針數(shù)組):" << * ((int*)&father) << endl << endl;


 cout << "得到第一個虛函數(shù)指針" << (int*)*(int*)&father << endl << endl;
 int* vptr = (int*)*(int*)&father;

 /*
 cout << "訪問第一個虛函數(shù)" << (int*)*(int*)&father << endl << endl;
 func_t test1 = (func_t)(int*)*(int*)&father;
 test1();
 //或者
 ((func_t) * (vptr + 0))();


 存在的問題:
 1.虛函數(shù)指針之間相差的內(nèi)存為4,并不是為8
 2.這兩種訪問虛函數(shù)的方法都有訪問內(nèi)存錯誤
 */

 cout << "得到第二個虛函數(shù)指針" << (int*) *(int*)&father+1 << endl << endl;
 int* vfptr2 = vptr + 1;

 cout << "得到第三個虛函數(shù)指針" << (int*) *(int*)&father+2 << endl << endl;
 int* vfptr3 = vptr + 2;

 system("pause");
 return 0;
}

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

網(wǎng)上的代碼全部報錯,這個問題待解決

(2)使用繼承的虛函數(shù)表
#include <iostream>
using namespace std;

class Father {
public:
    virtual void func1() { cout << "Father::func1" << endl; }
    virtual void func2() { cout << "Father::func2" << endl; }
    virtual void func3() { cout << "Father::func3" << endl; }
	void func4() { cout << "非虛函數(shù):Father::func4" << endl; }
public: //為了便于測試,特別該用 public
    int x = 100;
    int y = 200;
};

class Son : public Father {
public:
    void func1() { cout << "Son::func1" << endl; }
    virtual void func5() { cout << "Son::func5" << endl; }
};

typedef void (*func_t)(void);

int main(void) {
    Father father;
    Son son;
    
    // 含有虛函數(shù)的對象的內(nèi)存中,最先存儲的就是“虛函數(shù)表”
    cout << "son 對象地址:" << (int*)&son << endl;
    
    int* vptr = (int*)*(int*)&son;
    cout << "虛函數(shù)表指針 vptr:" << vptr << endl;
    
    for (int i = 0; i < 4; i++) {
    	cout << "調(diào)用第" << i + 1 << "個虛函數(shù):";
    	((func_t) * (vptr + i))();
    }
    
    for (int i = 0; i < 2; i++) {
        // +4 是因為先存儲了虛表指針
        cout << *(int*)((int)&son + 4 + i * 4) << endl;
    }
    
    system("pause");
    return 0;
}

執(zhí)行效果:

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

內(nèi)存分布:

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

(3)多重繼承的虛函數(shù)表
#include <iostream>
using namespace std;
class Father {
public:
    virtual void func1() { cout << "Father::func1" << endl; }
    virtual void func2() { cout << "Father::func2" << endl; }
    virtual void func3() { cout << "Father::func3" << endl; }
    void func4() { cout << "非虛函數(shù):Father::func4" << endl; }
public:
    int x = 200;
    int y = 300;
    static int z;
};
	
class Mother {
public:
    virtual void handle1() { cout << "Mother::handle1" << endl; }
    virtual void handle2() { cout << "Mother::handle2" << endl; }
    virtual void handle3() { cout << "Mother::handle3" << endl; }
public: //為了便于測試,使用 public 權限
    int m = 400;
    int n = 500;
};

class Son : public Father, public Mother {
public:
    void func1() { cout << "Son::func1" << endl; }
    virtual void handle1() { cout << "Son::handle1" << endl; }
    virtual void func5() { cout << "Son::func5" << endl; }
};

int Father::z = 0;

typedef void(*func_t)(void);

int main(void) {
    Son son;
    int* vptr = (int*) * (int*)&son;
    
    cout << "第一個虛函數(shù)表指針:" << vptr << endl;
    for (int i = 0; i < 4; i++) {
        cout << "調(diào)用第" << i + 1 << "個虛函數(shù):";
        ((func_t) * (vptr + i))();
    }
    
    for (int i = 0; i < 2; i++) {
   		cout << *(int*)((int)&son + 4 + i * 4) << endl;
    }
    
    int* vptr2 = (int*) * ((int*)&son + 3);
    for (int i = 0; i < 3; i++) {
        cout << "調(diào)用第" << i + 1 << "個虛函數(shù):";
        ((func_t) * (vptr2 + i))();
    }
    
    for (int i = 0; i < 2; i++) {
    	cout << *(int*)((int)&son + 16 + i * 4) << endl;
    }
    
    system("pause");
    return 0;
}

執(zhí)行結(jié)果:

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

內(nèi)存分布:

【C++篇】OOP下部分:友元、運算符重載與多態(tài)

  • Father在前面同樣取決于聲明時的順序
  • 兩個類就有兩張?zhí)摵瘮?shù)表

??3.C++11 override和final

(1)override:

override 僅能用于修飾虛函數(shù)。

作用:

  • 提示程序的閱讀者,這個函數(shù)是重寫父類的功能。
  • 防止程序員在重寫父類的函數(shù)時,把函數(shù)名寫錯。

override 只需在函數(shù)聲明中使用,不需要在函數(shù)的實現(xiàn)中使用。

#include <iostream>
using namespace std;

class XiaoMi {
public:
	virtual void func() { cout << "XiaoMi::func" << endl; };
};

class XiaoMi2 : public XiaoMi {
public:
    void func() override {}
    //void func() override; 告訴程序員 func 是重寫父類的虛函數(shù)
    //void func1() override{} 錯誤!因為父類沒有 func1 這個虛函數(shù)
};

int main(void) {
    XiaoMi2 xiaomi;
    return 0;
}

(2)final:

  • 用來修飾類,讓該類不能被繼承,理解:使得該類終結(jié)!
class XiaoMi {
public:
	XiaoMi(){}
};

class XiaoMi2 final : public XiaoMi {
	XiaoMi2(){}
};

class XiaoMi3 : public XiaoMi2 { //不能把 XiaoMi2 作為基類
};
  • 用來修飾類的虛函數(shù),使得該虛函數(shù)在子類中,不能被重寫,理解:使得該功能終結(jié)!
class XiaoMi {
public:
	virtual void func() final;
};

void XiaoMi::func() { //不需要再寫 final
	cout << "XiaoMi::func" << endl;
}

class XiaoMi2 : public XiaoMi {
public:
	void func() {}; // 錯誤!不能重寫 func 函數(shù)
};

?(二)純虛函數(shù)和抽象類

什么時候使用純虛函數(shù)

某些類,在現(xiàn)實角度和項目實現(xiàn)角度,都不需要實例化(不需要創(chuàng)建它的對象),

這個類中定義的某些成員函數(shù),只是為了提供一個形式上的接口,準備讓子類來做具體的實現(xiàn)。此時,這個方法,就可以定義為“純虛函數(shù)”, 包含純虛函數(shù)的類,就稱為抽象類。

純虛函數(shù)的使用方法

用法:純虛函數(shù),使用 virtual 和 =0

#include <iostream>
#include <string>

using namespace std;

class Shape {
public:
        Shape(const string& color = "white") { this->color = color; }
        virtual float area() = 0; //不用做具體的實現(xiàn)
        string getColor() { return color; }
    private:
    	string color;
};

class Circle : public Shape {
public:
	Circle(float radius = 0, const string& color="White")
		:Shape(color), r(radius){}
	float area();
private:
	float r; //半徑
};

float Circle::area() {
	return 3.14 * r * r;
}

int main() {
    //使用抽象類創(chuàng)建對象非法!
    //Shape s;
    
    Circle c1(10);
    cout << c1.area() << endl;
    
    Shape* p = &c1;
    cout << p->area() << endl;
    
    system("pause");
    return 0;
}
  • 不能使用抽象類創(chuàng)建對象

  • 可以在實現(xiàn)文件中提供方法的定義,也可以在繼承的子類中實現(xiàn)這個虛函數(shù)

  • 父類聲明某純虛函數(shù)后,那么它的子類:

    要么實現(xiàn)這個純虛函數(shù) (最常見)

    要么繼續(xù)把這個純虛函數(shù)聲明為純虛函數(shù),這個子類也成為抽象類

    要么不對這個純虛函數(shù)做任何處理,等效于上一種情況(該方式不推薦)文章來源地址http://www.zghlxwxcb.cn/news/detail-496470.html


行文至此,落筆為終。文末擱筆,思緒駁雜。只道謝不道別。早晚復相逢,且祝諸君平安喜樂,萬事順意。

到了這里,關于【C++篇】OOP下部分:友元、運算符重載與多態(tài)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 【C++】詳解運算符重載,賦值運算符重載,++運算符重載

    【C++】詳解運算符重載,賦值運算符重載,++運算符重載

    目錄 前言 運算符重載 概念 目的 寫法 調(diào)用 注意事項 詳解注意事項 運算符重載成全局性的弊端 類中隱含的this指針 賦值運算符重載 賦值運算符重載格式 注意點 明晰賦值運算符重載函數(shù)的調(diào)用 連續(xù)賦值 傳引用與傳值返回 默認賦值運算符重載 前置++和后置++重載 先梳理一下

    2024年04月25日
    瀏覽(31)
  • C# 類class、繼承、多態(tài)性、運算符重載,相關練習題

    34.函數(shù)重載 35.幾個相同的函數(shù)? print() ,用于打印不同的數(shù)據(jù)類型。 ? 36.基類和派生類 ? 37.基類的初始化 ? 38.多重繼承 ? 39.動態(tài)多態(tài)性 ? 40.抽象性和虛方法 ? 41.通過虛方法 area() 來計算不同形狀圖像的面積 ? 42.運算符重載的實現(xiàn) ? @www.runoob.com?

    2024年02月09日
    瀏覽(32)
  • 【C++】運算符重載案例 - 字符串類 ⑤ ( 重載 大于 > 運算符 | 重載 小于 < 運算符 | 重載 右移 >> 運算符 - 使用全局函數(shù)重載 | 代碼示例 )

    使用 成員函數(shù) 實現(xiàn) 等于判斷 == 運算符重載 : 首先 , 寫出函數(shù)名 , 函數(shù)名規(guī)則為 \\\" operate \\\" 后面跟上要重載的運算符 , 要對 String a , b 對象對比操作 , 使用 大于 運算符 , 使用時用法為 a b ; 函數(shù)名是 operate ; 然后 , 根據(jù)操作數(shù) 寫出函數(shù)參數(shù) , 參數(shù)一般都是 對象的引用 ; 要對

    2024年02月07日
    瀏覽(27)
  • C++,運算符重載——關系運算符練習

    C++,運算符重載——關系運算符練習

    一、關系運算符重載 = = == != ?二、知識點整理 ?

    2024年02月11日
    瀏覽(24)
  • C++:重載運算符

    C++:重載運算符

    1.重載不能改變運算符運算的對象個數(shù) 2.重載不能改變運算符的優(yōu)先級別 3.重載不能改變運算符的結(jié)合性 4.重載運算符必須和用戶定義的自定義類型的對象一起使用,其參數(shù)至少應該有一個是類對象,或類對象的引用 5.重載運算符的功能要類似于該運算符作用于標準類型數(shù)據(jù)

    2024年02月10日
    瀏覽(20)
  • 【C++】運算符重載

    目錄 1. 基本概念 1.1 直接調(diào)用一個重載的運算符函數(shù) 1.2 某些運算符不應該被重載 1.3 使用與內(nèi)置類型一致的含義 1.4 賦值和復合賦值運算符 1.5 選擇作為成員或者非成員 2. 輸入和輸出運算符 2.1 輸出運算符重載 2.2 輸入運算符重載 3. 算術和關系運算符 3.1 算數(shù)運算符重載 3.2

    2024年02月11日
    瀏覽(32)
  • C++——運算符重載

    C++——運算符重載

    運算符重載,就是對已有的運算符重新進行定義,賦予其另一種功能,以適應不同的數(shù)據(jù)類型。 運算符重載的目的是讓語法更加簡潔 運算符重載不能改變本來寓意,不能改變基礎類型寓意 運算符重載的本質(zhì)是另一種函數(shù)調(diào)用(是編譯器去調(diào)用) 這個函數(shù)統(tǒng)一的名字叫opera

    2024年02月16日
    瀏覽(34)
  • 復習 --- C++運算符重載

    .5 運算符重載 運算符重載概念:對已有的運算符重新進行定義,賦予其另外一種功能,以適應不同的數(shù)據(jù)類型 4.5.1 加號運算符重載 作用:實現(xiàn)兩個自定義數(shù)據(jù)類型相加的運算 4.5.2 左移運算符重載 4.5.3遞增運算符重載 作用:通過重載遞增運算符,實現(xiàn)自己的整型數(shù)據(jù) 4.5.4 賦

    2024年02月07日
    瀏覽(26)
  • C++——類和對象3|日期類型|Cout運算符重載|Cin運算符重載|const成員|

    C++——類和對象3|日期類型|Cout運算符重載|Cin運算符重載|const成員|

    目錄 日期類型? Date.h? Date.cpp? Test.cpp? 實現(xiàn)Cout運算符重載? 實現(xiàn)Cin運算符重載? 根據(jù)日期算星期? 修改后完整代碼?? Date.h? Date.cpp? const成員 ?取地址及const取地址操作符重載 習題? 計算日期到天數(shù)轉(zhuǎn)換? ? ?一個類到底可以重載哪些運算符,要看哪些運算符對這個類型有

    2023年04月13日
    瀏覽(27)
  • C++語法——詳解運算符重載

    C++語法——詳解運算符重載

    運算符重載是C++的一個重要特性。有了運算符重載,在代碼編寫時能更好的實現(xiàn)封裝。 目錄 一.運算符重載介紹 二.運算符重載形式 (一).參數(shù) (二).返回值 (三).應用 三.特殊的運算符重載 (一).默認賦值運算符重載 (二).自增運算符A++與++A (三).流提取與流插入

    2023年04月25日
    瀏覽(44)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包