概述
整理码字不易,养成好习惯,点赞关注,你的支持就是我写下去的动力,谢谢老板。
本文为C++第三篇后续接着这篇文章写,大家可以持续关注,前三篇在主页
4.5 友元
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,怎么办?
解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。这一点从现实生活中也可以很好的理解:
比如你的家,有客厅,有你的卧室,那么你的客厅是Public的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的闺蜜好基友进去。
程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
4.5.1 友元语法
- friend关键字只出现在声明处
- 其他类、类成员函数、全局函数都可声明为友元
- 友元函数不是类的成员,不带this指针
- 友元函数可访问对象任意成员属性,包括私有属性
class Building; //友元类 class MyFriend{ public: //友元成员函数 void LookAtBedRoom(Building& building); void PlayInBedRoom(Building& building); }; class Building{ //全局函数做友元函数 friend void CleanBedRoom(Building& building); #if 0 //成员函数做友元函数 friend void MyFriend::LookAtBedRoom(Building& building); friend void MyFriend::PlayInBedRoom(Building& building); #else //友元类 friend class MyFriend; #endif public: Building(); public: string mSittingRoom; private: string mBedroom; };
void MyFriend::LookAtBedRoom(Building& building){ cout << "我的朋友参观" << building.mBedroom << endl; } void MyFriend::PlayInBedRoom(Building& building){ cout << "我的朋友玩耍在" << building.mBedroom << endl; }
//友元全局函数 void CleanBedRoom(Building& building){ cout << "友元全局函数访问" << building.mBedroom << endl; }
Building::Building(){ this->mSittingRoom = "客厅"; this->mBedroom = "卧室"; }
int main(){
Building building; MyFriend myfriend;
CleanBedRoom(building); myfriend.LookAtBedRoom(building); myfriend.PlayInBedRoom(building);
system("pause"); return EXIT_SUCCESS; } |
[友元类注意]
|
思考: c++是纯面向对象的吗?
如果一个类被声明为friend,意味着它不是这个类的成员函数,却可以修改这个类的私有成员,而且必须列在类的定义中,因此他是一个特权函数。c++不是完全的面向对象语言,而只是一个混合产品。增加friend关键字只是用来解决一些实际问题,这也说明这种语言是不纯的。毕竟c++设计的目的是为了实用性,而不是追求理想的抽象。 --- Thinking in C++ |
4.5.2 课堂练习
请编写电视机类,电视机有开机和关机状态,有音量,有频道,提供音量操作的方法,频道操作的方法。由于电视机只能逐一调整频道,不能指定频道,增加遥控类,遥控类除了拥有电视机已有的功能,再增加根据输入调台功能。 |
提示:遥控器可作为电视机类的友元类。
class Remote;
class Television{ friend class Remote; public: enum{ On,Off }; //电视状态 enum{ minVol,maxVol = 100 }; //音量从0到100 enum{ minChannel = 1,maxChannel = 255 }; //频道从1到255 Television(){ mState = Off; mVolume = minVol; mChannel = minChannel; }
//打开电视机 void OnOrOff(){ this->mState = (this->mState == On ? Off : On); } //调高音量 void VolumeUp(){ if (this->mVolume >= maxVol){ return; } this->mVolume++; } //调低音量 void VolumeDown(){ if (this->mVolume <= minVol){ return; } this->mVolume--; } //更换电视频道 void ChannelUp(){ if (this->mChannel >= maxChannel){ return; } this->mChannel++; } void ChannelDown(){ if (this->mChannel <= minChannel){ return; } this->mChannel--; } //展示当前电视状态信息 void ShowTeleState(){ cout << "开机状态:" << (mState == On ? "已开机" : "已关机") << endl; if (mState == On){ cout << "当前音量:" << mVolume << endl; cout << "当前频道:" << mChannel << endl; } cout << "-------------" << endl; } private: int mState; //电视状态,开机,还是关机 int mVolume; //电视机音量 int mChannel; //电视频道 };
//电视机调台只能一个一个的调,遥控可以指定频道 //电视遥控器 class Remote{ public: Remote(Television* television){ pTelevision = television; } public: void OnOrOff(){ pTelevision->OnOrOff(); } //调高音量 void VolumeUp(){ pTelevision->VolumeUp(); } //调低音量 void VolumeDown(){ pTelevision->VolumeDown(); } //更换电视频道 void ChannelUp(){ pTelevision->ChannelUp(); } void ChannelDown(){ pTelevision->ChannelDown(); } //设置频道 遥控新增功能 void SetChannel(int channel){ if (channel < Television::minChannel || channel > Television::maxChannel){ return; } pTelevision->mChannel = channel; }
//显示电视当前信息 void ShowTeleState(){ pTelevision->ShowTeleState(); } private: Television* pTelevision; };
//直接操作电视 void test01(){
Television television; television.ShowTeleState(); television.OnOrOff(); //开机 television.VolumeUp(); //增加音量+1 television.VolumeUp(); //增加音量+1 television.VolumeUp(); //增加音量+1 television.VolumeUp(); //增加音量+1 television.ChannelUp(); //频道+1 television.ChannelUp(); //频道+1 television.ShowTeleState(); }
//通过遥控操作电视 void test02(){ //创建电视 Television television; //创建遥控 Remote remote(&television); remote.OnOrOff(); remote.ChannelUp();//频道+1 remote.ChannelUp();//频道+1 remote.ChannelUp();//频道+1 remote.VolumeUp();//音量+1 remote.VolumeUp();//音量+1 remote.VolumeUp();//音量+1 remote.VolumeUp();//音量+1 remote.ShowTeleState(); } |
4.5 强化训练(数组类封装)
MyArray.h
#ifndef MYARRAY_H #define MYARRAY_H
class MyArray{ public: //无参构造函数,用户没有指定容量,则初始化为100 MyArray(); //有参构造函数,用户指定容量初始化 explicit MyArray(int capacity); //用户操作接口 //根据位置添加元素 void SetData(int pos, int val); //获得指定位置数据 int GetData(int pos); //尾插法 void PushBack(int val); //获得长度 int GetLength(); //析构函数,释放数组空间 ~MyArray(); private: int mCapacity; //数组一共可容纳多少个元素 int mSize; //当前有多少个元素 int* pAdress; //指向存储数据的空间 };
#endif |
MyArray.cpp
#include"MyArray.h"
MyArray::MyArray(){ this->mCapacity = 100; this->mSize = 0; //在堆开辟空间 this->pAdress = new int[this->mCapacity]; } //有参构造函数,用户指定容量初始化 MyArray::MyArray(int capacity){ this->mCapacity = capacity; this->mSize = 0; //在堆开辟空间 this->pAdress = new int[capacity]; } //根据位置添加元素 void MyArray::SetData(int pos, int val){ if (pos < 0 || pos > mCapacity - 1){ return; } pAdress[pos] = val; } //获得指定位置数据 int MyArray::GetData(int pos){ return pAdress[pos]; } //尾插法 void MyArray::PushBack(int val){ if (mSize >= mCapacity){ return; } this->pAdress[mSize] = val; this->mSize++; } //获得长度 int MyArray::GetLength(){ return this->mSize; } //析构函数,释放数组空间 MyArray::~MyArray(){ if (this->pAdress != nullptr){ delete[] this->pAdress; } } |
TestMyArray.cpp
#include"MyArray.h"
void test(){ //创建数组 MyArray myarray(50); //数组中插入元素 for (int i = 0; i < 50; i++){ //尾插法 myarray.PushBack(i); //myarray.SetData(i, i); } //打印数组中元素 for (int i = 0; i < myarray.GetLength(); i++){ cout << myarray.GetData(i) << " "; } cout << endl; } |
4.6 运算符重载
4.6.1 运算符重载基本概念
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
运算符重载(operator overloading)只是一种”语法上的方便”,也就是它只是另一种函数调用的方式。 |
在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
语法:
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。
|
[两个极端] 有些人很容易滥用运算符重载。它确实是一个有趣的工具。但是应该注意,它仅仅是一种语法上的方便而已,是另外一种函数调用的方式。从这个角度来看,只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比我们写代码多多了)才有理由重载运算符。如果不是这样,就改用其他更易用,更易读的方式。 对于运算符重载,另外一个常见的反应是恐慌:突然之间,C运算符的含义变得不同寻常了,一切都变了,所有C代码的功能都要改变!并非如此,对于内置的数据类型的表示总的所有运算符是不可能改变的。
|
4.6.2 运算符重载碰上友元函数
友元函数是一个全局函数,和我们上例写的全局函数类似,只是友元函数可以访问某个类私有数据。
案例: 重载左移操作符(<<),使得cout可以输出对象。
class Person{ friend ostream& operator<<(ostream& os, Person& person); public: Person(int id,int age){ mID = id; mAge = age; } private: int mID; int mAge; };
ostream& operator<<(ostream& os, Person& person){ os << "ID:" << person.mID << " Age:" << person.mAge; return os; }
int main(){
Person person(1001, 30); //cout << person; //cout.operator+(person) cout << person << " | " << endl;
return EXIT_SUCCESS; } |
4.6.3 可重载的运算符
几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能使用C中当前没有意义的运算符(例如用**求幂)不能改变运算符优先级,不能改变运算符的参数个数。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清寓语意。
4.6.4 自增自减(++/--)运算符重载
重载的++和--运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).
class Complex{ friend ostream& operator<<(ostream& os,Complex& complex){ os << "A:" << complex.mA << " B:" << complex.mB << endl; return os; } public: Complex(){ mA = 0; mB = 0; } //重载前置++ Complex& operator++(){ mA++; mB++; return *this; } //重载后置++ Complex operator++(int){ Complex temp; temp.mA = this->mA; temp.mB = this->mB; mA++; mB++; return temp; } //前置-- Complex& operator--(){ mA--; mB--; return *this; } //后置-- Complex operator--(int){ Complex temp; temp.mA = mA; temp.mB = mB; mA--; mB--; return temp; } void ShowComplex(){ cout << "A:" << mA << " B:" << mB << endl; } private: int mA; int mB; };
void test(){ Complex complex; complex++; cout << complex; ++complex; cout << complex;
Complex ret = complex++; cout << ret; cout << complex;
cout << "------" << endl; ret--; --ret; cout << "ret:" << ret; complex--; --complex; cout << "complex:" << complex; } |
优先使用++和--的标准形式,优先调用前置++。 如果定义了++c,也要定义c++,递增操作符比较麻烦,因为他们都有前缀和后缀形式,而两种语义略有不同。重载operator++和operator--时应该模仿他们对应的内置操作符。 对于++和--而言,后置形式是先返回,然后对象++或者--,返回的是对象的原值。前置形式,对象先++或--,返回当前对象,返回的是新对象。其标准形式为:
调用代码时候,要优先使用前缀形式,除非确实需要后缀形式返回的原值,前缀和后缀形式语义上是等价的,输入工作量也相当,只是效率经常会略高一些,由于前缀形式少创建了一个临时对象。 |
4.6.5 指针运算符(*、->)重载
class Person{ public: Person(int param){ this->mParam = param; } void PrintPerson(){ cout << "Param:" << mParam << endl; } private: int mParam; };
class SmartPointer{ public: SmartPointer(Person* person){ this->pPerson = person; } //重载指针的->、*操作符 Person* operator->(){ return pPerson; } Person& operator*(){ return *pPerson; } ~SmartPointer(){ if (pPerson != NULL){ delete pPerson; } } public: Person* pPerson; };
void test01(){
//Person* person = new Person(100); //如果忘记释放,那么就会造成内存泄漏
SmartPointer pointer(new Person(100)); pointer->PrintPerson(); } |
4.6.6 赋值(=)运算符重载
赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
class Person{ friend ostream& operator<<(ostream& os,const Person& person){ os << "ID:" << person.mID << " Age:" << person.mAge << endl; return os; } public: Person(int id,int age){ this->mID = id; this->mAge = age; } //重载赋值运算符 Person& operator=(const Person& person){ this->mID = person.mID; this->mAge = person.mAge; return *this; } private: int mID; int mAge; };
//1. =号混淆的地方 void test01(){ Person person1(10, 20); Person person2 = person1; //调用拷贝构造 //如果一个对象还没有被创建,则必须初始化,也就是调用构造函数 //上述例子由于person2还没有初始化,所以会调用构造函数 //由于person2是从已有的person1来创建的,所以只有一个选择 //就是调用拷贝构造函数 person2 = person1; //调用operator=函数 //由于person2已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符 } //2. 赋值重载案例 void test02(){ Person person1(20, 20); Person person2(30, 30); cout << "person1:" << person1; cout << "person2:" << person2; person2 = person1; cout << "person2:" << person2; } //常见错误,当准备给两个相同对象赋值时,应该首先检查一下这个对象是否对自身赋值了 //对于本例来讲,无论如何执行这些赋值运算都是无害的,但如果对类的实现进行修改,那么将会出现差异; //3. 类中指针 class Person2{ friend ostream& operator<<(ostream& os, const Person2& person){ os << "Name:" << person.pName << " ID:" << person.mID << " Age:" << person.mAge << endl; return os; } public: Person2(char* name,int id, int age){ this->pName = new char[strlen(name) + 1]; strcpy(this->pName, name); this->mID = id; this->mAge = age; } #if 1 //重载赋值运算符 Person2& operator=(const Person2& person){
//注意:由于当前对象已经创建完毕,那么就有可能pName指向堆内存 //这个时候如果直接赋值,会导致内存没有及时释放 if (this->pName != NULL){ delete[] this->pName; }
this->pName = new char[strlen(person.pName) + 1]; strcpy(this->pName,person.pName); this->mID = person.mID; this->mAge = person.mAge; return *this; } #endif //析构函数 ~Person2(){ if (this->pName != NULL){ delete[] this->pName; } } private: char* pName; int mID; int mAge; };
void test03(){ Person2 person1("John",20, 20); Person2 person2("Edward",30, 30); cout << "person1:" << person1; cout << "person2:" << person2; person2 = person1; cout << "person2:" << person2; } |
如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造,进行简单值拷贝。 |
4.6.7 等于和不等于(==、!=)运算符重载
class Complex{ public: Complex(char* name,int id,int age){ this->pName = new char[strlen(name) + 1]; strcpy(this->pName, name); this->mID = id; this->mAge = age; } //重载==号操作符 bool operator==(const Complex& complex){ if (strcmp(this->pName,complex.pName) == 0 && this->mID == complex.mID && this->mAge == complex.mAge){ return true; } return false; } //重载!=操作符 bool operator!=(const Complex& complex){ if (strcmp(this->pName, complex.pName) != 0 || this->mID != complex.mID || this->mAge != complex.mAge){ return true; } return false; } ~Complex(){ if (this->pName != NULL){ delete[] this->pName; } } private: char* pName; int mID; int mAge; }; void test(){ Complex complex1("aaa", 10, 20); Complex complex2("bbb", 10, 20); if (complex1 == complex2){ cout << "相等!" << endl; } if (complex1 != complex2){ cout << "不相等!" << endl; } } |
4.6.8 函数调用符号()重载
class Complex{ public: int Add(int x,int y){ return x + y; } int operator()(int x,int y){ return x + y; } }; void test01(){ Complex complex; cout << complex.Add(10,20) << endl; //对象当做函数来调用 cout << complex(10, 20) << endl; } |
4.6.9 不要重载&&、||
不能重载operator&& 和 operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了--而且能够保证不需要。我们都已经习惯这种方便的特性了。
我们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。
class Complex{ public: Complex(int flag){ this->flag = flag; } Complex& operator+=(Complex& complex){ this->flag = this->flag + complex.flag; return *this; } bool operator&&(Complex& complex){ return this->flag && complex.flag; } public: int flag; }; int main(){
Complex complex1(0); //flag 0 Complex complex2(1); //flag 1
//原来情况,应该从左往右运算,左边为假,则退出运算,结果为假 //这边却是,先运算(complex1+complex2),导致,complex1的flag变为complex1+complex2的值, complex1.a = 1 // 1 && 1 //complex1.operator&&(complex1.operator+=(complex2)) if (complex1 && (complex1 += complex2)){ cout << "真!" << endl; } else{ cout << "假!" << endl; }
return EXIT_SUCCESS; } |
根据内置&&的执行顺序,我们发现这个案例中执行顺序并不是从左向右,而是先右猴左,这就是不满足我们习惯的特性了。由于complex1 += complex2先执行,导致complex1 本身发生了变化,初始值是0,现在经过+=运算变成1,1 && 1输出了真。
4.6.10 符号重载总结
- =, [], () 和 -> 操作符只能通过成员函数进行重载
- << 和 >>只能通过全局函数配合友元函数进行重载
- 不要重载 && 和 || 操作符,因为无法实现短路规则
常规建议
4.6.10 强化训练_字符串类封装
MyString.h
#define _CRT_SECURE_NO_WARNINGS #pragma once #include <iostream> using namespace std;
class MyString { friend ostream& operator<< (ostream & out, MyString& str); friend istream& operator>>(istream& in, MyString& str);
public: MyString(const char *); MyString(const MyString&); ~MyString();
char& operator[](int index); //[]重载
//=号重载 MyString& operator=(const char * str); MyString& operator=(const MyString& str);
//字符串拼接 重载+号 MyString operator+(const char * str ); MyString operator+(const MyString& str);
//字符串比较 bool operator== (const char * str); bool operator== (const MyString& str); private: char * pString; //指向堆区空间 int m_Size; //字符串长度 不算' |