友情鏈接: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ī)則:
-
為了防止對標準類型進行運算符重載,C++規(guī)定重載運算符的操作對象至少有一個不是標準類型,而是用戶自定義的類型比如不能重載 1+2
但是可以重載
cow + 2 和 2 + cow // cow 是自定義的對象
-
不能改變原運算符的語法規(guī)則, 比如不能把雙目運算符重載為單目運算
-
不能修改運算符的優(yōu)先級。因此,如果將加號運算符重載成將兩個類相加,則新的運算符與原來的加號具有相同的優(yōu)先級。
-
不能創(chuàng)建新運算符。例如,不能定義operator **( )函數(shù)來表示求冪。
-
不能對以下這四種運算符,使用友元函數(shù)進行重載
= 賦值運算符,()函數(shù)調(diào)用運算符,[ ]下標運算符,->通過指針訪問類成員
大多數(shù)運算符都可以通過成員或非成員函數(shù)(一般都是友元函數(shù))進行重載,但這四個運算符只能通過成員函數(shù)進行重載。
重載的運算符(有些例外情況)不必是成員函數(shù)
-
不能對禁止重載的運算符進行重載
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ù)來傳遞。
兩種方式的選擇:
-
一般情況下,單目運算符重載,使用成員函數(shù)進行重載更方便(不用寫參數(shù))
-
一般情況下,雙目運算符重載,使用友元函數(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)還有兩個條件:
-
必須通過基類的指針或者引用調(diào)用虛函數(shù)。
-
被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對基類的虛函數(shù)進行重寫。
-
-
多態(tài)的本質(zhì):
使用virtual指明虛函數(shù),如果方法是通過引用或指針而不是對象調(diào)用的,它將確定使用哪一種方法。如果沒有使用關鍵字virtual,程序?qū)⒏鶕?jù)引用類型或指針類型選擇方法;如果使用了virtual,程序?qū)⒏鶕?jù)引用或指針指向的對象的類型來選擇方法。
程序執(zhí)行時,父類指針指向父類對象,或子類對象時,在形式上是無法分辨的!只有通過多態(tài)機制,才能執(zhí)行真正對應的方法。
-
虛函數(shù):
- 虛函數(shù)的定義:
在函數(shù)的返回類型之前使用 virtual,只在成員函數(shù)的聲明中添加 virtual, 在成員函數(shù)的實現(xiàn)中不要加 virtual
- 虛函數(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))
所以這里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ù)表:
-
對象內(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ù)的指針,最后用*解引用即可
//通過虛指針訪問虛函數(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; }
網(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í)行效果:
內(nè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é)果:
內(nè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ù),這個子類也成為抽象類文章來源:http://www.zghlxwxcb.cn/news/detail-496470.html
要么不對這個純虛函數(shù)做任何處理,等效于上一種情況(該方式不推薦)文章來源地址http://www.zghlxwxcb.cn/news/detail-496470.html
到了這里,關于【C++篇】OOP下部分:友元、運算符重載與多態(tài)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!