目录
- 一. C++面向对象模型初探
- 1 .成员变量和函数的存储
- 二 this指针
- 1 )this指针工作原理
- 2 )this指针的使用
- 3) const修饰成员函数
- 4 )const修饰对象(常对象)
- 3.友元
- 1 )友元语法
- 2) 课堂练习
- 4 强化训练(数组类封装)
- 四 运算符重载(没整理完)
- 1 )运算符重载基本概念
- 2 运算符重载碰上友元函数
- 3 可重载的运算符
- 4 前置和后置(++/--)运算符重载
- 5 赋值(=)运算符重载
- 6 指针运算符(*、->)重载
- 7 不要重载&&、||
- 8 强化训练_字符串类封装
- 9 附录:运算符和结合性
- 五. 继承和派生
- 1 继承概述
- 1.1 为什么需要继承
- 1.2 继承基本概念
- 1.3 派生类定义
- 2 派生类访问控制
- 3 继承中的构造和析构
- 3.1 继承中的对象模型
- 3.2 对象构造和析构的调用原则
- 4.继承中同名成员的处理方法
- 5. 非自动继承的函数
- 6. 继承中的静态成员特性
- 7. 多继承
- 1 多继承概念
- 2 菱形继承和虚继承
- 3 虚继承实现原理
- 六 多态
- 1 多态基本概念
- 2 向上类型转换及问题
- 2.1 问题抛出
- 2.2 问题解决思路
- 2.3 问题解决方案(虚函数,vitual function)
- 3 C++如何实现动态绑定
- 4 抽象基类和纯虚函数
- 5 纯虚函数和多继承
- 6 虚析构函数
- 6.1 虚析构函数作用
- 6.2 纯虚析构函数
- 七. 重写 重载 重定义
一. C++面向对象模型初探
1 .成员变量和函数的存储
在c语言中, 变量和函数“分开来声明的,也就是说,语言本身并没有支持“数据”和“函数”之间的关联性我们把这种程序方法称为“程序性的”,由一组“分布在各个以功能为导航的函数中”的算法驱动,它们处理的是共同的外部数据。
c++实现了“封装”,那么数据(成员属性)和操作(成员函数)是什么样的呢?
“数据”和“处理数据的操作(函数)”是分开存储的。
- c++中的非静态数据成员直接内含在类对象中,就像c struct一样。
- 成员函数(member function)虽然内含在class声明之内,却不出现在对象中。
- 每一个非内联成员函数(non-inline member function)只会诞生一份函数实例.
class MyClass01{
public:int mA;
};class MyClass02{
public:int mA;static int sB;
};class MyClass03{
public:void printMyClass(){cout << "hello world!" << endl;}
public:int mA;static int sB;
};class MyClass04{
public:void printMyClass(){cout << "hello world!" << endl;}static void ShowMyClass(){cout << "hello world!" << endl;}
public:int mA;static int sB;
};int main(){MyClass01 mclass01;MyClass02 mclass02;MyClass03 mclass03;MyClass04 mclass04;cout << "MyClass01:" << sizeof(mclass01) << endl; //4//静态数据成员并不保存在类对象中cout << "MyClass02:" << sizeof(mclass02) << endl; //4//非静态成员函数不保存在类对象中cout << "MyClass03:" << sizeof(mclass03) << endl; //4//静态成员函数也不保存在类对象中cout << "MyClass04:" << sizeof(mclass04) << endl; //4return EXIT_SUCCESS;
}
MyClass01:4
MyClass02:4
MyClass03:4
MyClass04:4
通过上面的案例,我们可以的得出:C++类对象中的变量和函数是分开存储。
二 this指针
1 )this指针工作原理
通过上例我们知道,c++的数据和操作也是分开存储,并且每一个非内联成员函数(non-inline member function)只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。This指针指向被调用的成员函数所属的对象。
c++规定,this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址,也就是说虽然我们没有写上this指针,编译器在编译的时候也是会加上的。因此this也称为“指向本对象的指针”,this指针并不是对象的一部分,不会影响sizeof(对象)的结果。
this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,在外部看来,每一个对象都拥有自己的函数成员。一般情况下,并不写this,而是让系统进行默认设置。
this指针永远指向当前对象。
成员函数通过this指针即可知道操作的是那个对象的数据。This指针是一种隐含指针,它隐含于每个类的非静态成员函数中。This指针无需定义,直接使用即可。
注意:静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量。
c++编译器对普通成员函数的内部处理
2 )this指针的使用
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this.
class Person{
public://1. 当形参名和成员变量名一样时,this指针可用来区分Person(string name,int age){//name = name;//age = age; //输出错误this->name = name;this->age = age;}//2. 返回对象本身的引用//重载赋值操作符//其实也是两个参数,其中隐藏了一个this指针Person PersonPlusPerson(Person& person){string newname = this->name + person.name;int newage = this->age + person.age;Person newperson(newname, newage);return newperson;}void ShowPerson(){cout << "Name:" << name << " Age:" << age << endl;}
public:string name;int age;
};//3. 成员函数和全局函数(Perosn对象相加)
Person PersonPlusPerson(Person& p1,Person& p2){string newname = p1.name + p2.name;int newage = p1.age + p2.age;Person newperson(newname,newage);return newperson;
}int main(){Person person("John",100);person.ShowPerson();cout << "---------" << endl;Person person1("John",20);Person person2("001", 10);//1.全局函数实现两个对象相加Person person3 = PersonPlusPerson(person1, person2);person1.ShowPerson();person2.ShowPerson();person3.ShowPerson();//2. 成员函数实现两个对象相加Person person4 = person1.PersonPlusPerson(person2);person4.ShowPerson();system("pause");return EXIT_SUCCESS;
}
3) const修饰成员函数
- 用const修饰的成员函数时,const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量,
- 当成员变量类型符前用mutable修饰时例外。
//const修饰成员函数
class Person{
public:Person(){this->mAge = 0;this->mID = 0;}//在函数括号后面加上const,修饰成员变量不可修改,除了mutable变量void sonmeOperate() const{//this->mAge = 200; //mAge不可修改this->mID = 10; //const Person* const tihs;}void ShowPerson(){cout << "ID:" << mID << " mAge:" << mAge << endl;}
private:int mAge;mutable int mID;
};int main(){Person person;person.sonmeOperate();person.ShowPerson();system("pause");return EXIT_SUCCESS;
}
4 )const修饰对象(常对象)
- 常对象只能调用const的成员函数
- 常对象可访问 const 或非 const 数据成员,不能修改,除非成员用mutable修饰
class Person{
public:Person(){this->mAge = 0;this->mID = 0;}void ChangePerson() const{//mAge = 100; //mAge不可修改mID = 100;}void ShowPerson(){cout << "ID:" << this->mID << " Age:" << this->mAge << endl;}public:int mAge;mutable int mID;
};void test(){ const Person person;//1. 可访问数据成员cout << "Age:" << person.mAge << endl;//person.mAge = 300; //不可修改person.mID = 1001; //但是可以修改mutable修饰的成员变量//2. 只能访问const修饰的函数//person.ShowPerson();person.ChangePerson();
}
3.友元
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,怎么办?
解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。这一点从现实生活中也可以很好的理解:
比如你的家,有客厅,有你的卧室,那么你的客厅是Public的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的闺蜜好基友进去。
程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
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;
}
[友元类注意]
- 1.友元关系不能被继承。
- 2.友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友。
- 3.友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。
思考: c++是纯面向对象的吗?
如果一个类被声明为friend,意味着它不是这个类的成员函数,却可以修改这个类的私有成员,而且必须列在类的定义中,因此他是一个特权函数。c++不是完全的面向对象语言,而只是一个混合产品。增加friend关键字只是用来解决一些实际问题,这也说明这种语言是不纯的。毕竟c++设计的目的是为了实用性,而不是追求理想的抽象。
— Thinking in C++
2) 课堂练习
请编写电视机类,电视机有开机和关机状态,有音量,有频道,提供音量操作的方法,频道操作的方法。由于电视机只能逐一调整频道,不能指定频道,增加遥控类,遥控类除了拥有电视机已有的功能,再增加根据输入调台功能。
提示:遥控器类可作为电视机类的友元类。
class Remote;class Television{friend class Remote;
public:enum{ On,Off }; //电视状态enum{ minVol,maxVol = 100 }; //音量从0到100enum{ minChannel = 1,maxChannel = 255 }; //频道从1到255Television(){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(); //增加音量+1television.VolumeUp(); //增加音量+1television.VolumeUp(); //增加音量+1television.VolumeUp(); //增加音量+1television.ChannelUp(); //频道+1television.ChannelUp(); //频道+1television.ShowTeleState();
}//通过遥控操作电视
void test02(){//创建电视Television television;//创建遥控Remote remote(&television);remote.OnOrOff();remote.ChannelUp();//频道+1remote.ChannelUp();//频道+1remote.ChannelUp();//频道+1remote.VolumeUp();//音量+1remote.VolumeUp();//音量+1remote.VolumeUp();//音量+1remote.VolumeUp();//音量+1remote.ShowTeleState();
}
4 强化训练(数组类封装)
MyArray.h
#ifndef MYARRAY_H
#define MYARRAY_Hclass MyArray{
public://无参构造函数,用户没有指定容量,则初始化为100MyArray();//有参构造函数,用户指定容量初始化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;
}
四 运算符重载(没整理完)
1 )运算符重载基本概念
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。(运算符重载不能改变本来寓意,不能改变基础类型寓意)
运算符重载(operator overloading)只是一种”语法上的方便”,也就是它只是另一种函数调用的方式。
在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
语法:
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。
- 运算符是一元(一个参数)的还是二元(两个参数);
- 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数)
代码:示例代码\49 []运算符重载
[两个极端]
-
- 有些人很容易滥用运算符重载。它确实是一个有趣的工具。但是应该注意,它仅仅是一种语法上的方便而已,是另外一种函数调用的方式。从这个角度来看,只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比我们写代码多多了)才有理由重载运算符。如果不是这样,就改用其他更易用,更易读的方式。
-
- 对于运算符重载,另外一个常见的反应是恐慌:突然之间,C运算符的含义变得不同寻常了,一切都变了,所有C代码的功能都要改变!并非如此,对于内置的数据类型的表示总的所有运算符是不可能改变的。我们不能重载如下的运算符改变其行为。
1 + 4;
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;
}
3 可重载的运算符
几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能使用C中当前没有意义的运算符(例如用**求幂)不能改变运算符优先级,不能改变运算符的参数个数。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清寓语意。
除了赋值号(=)外,基类中被重载的操作符都将被派生类继承。
特殊运算符
- =, [], () 和 -> 操作符只能通过成员函数进行重载
- << 和 >> 操作符最好通过友元函数进行重载
- 不要重载 && 和 || 操作符,因为无法实现短路规则
常规建议
4 前置和后置(++/–)运算符重载
重载的++和–运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(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–时应该模仿他们对应的内置操作符。
对于++和–而言,后置形式是先返回,然后对象++或者–,返回的是对象的原值。前置形式,对象先++或–,返回当前对象,返回的是新对象。其标准形式为:
- 优先使用++和–的标准形式,优先调用前置++。
调用代码时候,要优先使用前缀形式,除非确实需要后缀形式返回的原值,前缀和后缀形式语义上是等价的,输入工作量也相当,只是效率经常会略高一些,由于前缀形式少创建了一个临时对象。
5 赋值(=)运算符重载
赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
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;
}
为什么operator=返回一个reference to this ?
为了实现连续赋值,赋值操作符必须返回一个引用指向操作符的左侧实参。这是你为class实现赋值操作符必须遵循的协议。这个协议不仅适用于标准的赋值形式,也适用于+=、-=、=等等。
注意,这只是一个协议,并无请执行。如果不遵循它,可能代码一样通过编译。然而这份协议被所有内置类型和标准程序库所提供的类型如string、vector等所遵守。因此除非你有一个标新立异的好理由,不然还是随众吧。
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;
}
//重载赋值运算符
Person operator=(int x){
this->mID = x;
this->mAge = x;
return *this;
}
private:
int mID;
int mAge;
};
void test(){
Person person1(20, 20);
Person person2(30, 30);
cout << “person1:” << person1;
cout << “person2:” << person2;
//由于person2 = person1返回的是临时对象,所以赋值为10并没有改变person2对象
(person2 = person1) = 10;
cout << “person2:” << person2;
}
如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造,进行简单值拷贝。
6 指针运算符(*、->)重载
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();
}
7 不要重载&&、||
不能重载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);
Complex complex2(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输出了真。
8 强化训练_字符串类封装
MyString.h
#ifndef MYSTRING_H
#define MYSTRING_H
#include
class MyString{
public:
//构造和析构
MyString();
MyString(const char* p);
MyString(const MyString& obj);
~MyString();
//普通成员函数
const char* c_str() const;
int length(); //获取字符串长度
//运算符重载<< 重载,友元函数
friend ostream& operator<<(ostream& cout, MyString& obj);
friend void operator>>(const char* str, MyString& obj);
// =, [] 重载
MyString& operator=(const MyString& obj);
MyString& operator=(const char* p);
char& operator[](int index);
//重载+=、+
MyString& operator+=(MyString& str);
MyString& operator+=(const char* s);
MyString operator+(MyString& str);
MyString operator+(const char* str);
// , != 重载
bool operator(const char* p);
bool operator!=(const char* p);
bool operator==(MyString& obj);
bool operator!=(MyString& obj);
private:
char* pAddress;
int mLength;
};
#endif
MyString.cpp
#include “MyString.h”
//构造和析构
MyString::MyString(){
this->mLength = 0;
this->pAddress = new char[1];
this->pAddress[0] = ‘\0’;
}
MyString::MyString(const char* p){
this->mLength = strlen§;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress,p);
}
MyString::MyString(const MyString& obj){
this->mLength = obj.mLength;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress, obj.pAddress);
}
MyString::~MyString(){
if (this->pAddress != NULL){
delete[] this->pAddress;
}
}
const char* MyString::c_str() const{
return this->pAddress;
}
int MyString::length(){
return this->mLength;
}
//运算符重载<< 重载,友元函数
ostream& operator<<(ostream& out, MyString& obj){
out << obj.pAddress;
return out;
}
void operator>>(const char* str, MyString& obj){
if (obj.pAddress != NULL){
delete[] obj.pAddress;
}
obj.pAddress = new char[strlen(str) + 1];
for (int i = 0; i < strlen(str) + 1;i ++){
obj.pAddress[i] = ‘\0’;
}
strcpy(obj.pAddress,str);
}
// =,[] 重载
MyString& MyString::operator=(const MyString& obj){
if (this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
}
this->mLength = obj.mLength;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress,obj.pAddress);
return this;
}
MyString& MyString::operator=(const char p){
if (this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
}
this->mLength = strlen§;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress, p);
return *this;
}
char& MyString::operator[](int index){
return this->pAddress[index];
}
//重载+=、+
MyString& MyString::operator+=(MyString& str){
//判断追加的字符串是否为空
if (str.mLength == 0){
return this;
}
//计算两个字符串总长
this->mLength = this->mLength + str.mLength;
//申请两个字符串长度的空间
char pTemp = new char[this->mLength + 1];
//初始化数组
for (int i = 0; i < this->mLength + 1;i++){
pTemp[i] = ‘\0’;
}
//拷贝两个字符串到新空间中
char* p = pTemp;
strcat(p, this->pAddress);
strcat(p, str.pAddress);
//释放旧空间
if (this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
}
//更新pAddress指针
this->pAddress = pTemp;
return *this;
}
MyString& MyString::operator+=(const char* s){
//判断追加的字符串是否为空
if (s == NULL || strlen(s) == 0){
return *this;
}
//计算两个字符串总长
this->mLength = this->mLength + strlen(s);
//申请两个字符串长度的空间
char* pTemp = new char[this->mLength + 1];
//初始化数组
for (int i = 0; i < this->mLength + 1;i++){pTemp[0] = '\0';
}
//拷贝两个字符串到新空间中
strcat(pTemp, this->pAddress);
strcat(pTemp, s);
//释放旧空间
if (this->pAddress != NULL){delete[] this->pAddress;this->pAddress = NULL;
}
//更新指针
this->pAddress = pTemp;return *this;
}
MyString MyString::operator+(MyString& str){
if (str.mLength == 0){return *this;
}MyString tempString;tempString.mLength = this->mLength + str.mLength;
tempString.pAddress = new char[tempString.mLength + 1];
//初始化数组
for (int i = 0; i < tempString.mLength + 1;i++){tempString.pAddress[i] = '\0';
}
strcat(tempString.pAddress,this->pAddress);
strcat(tempString.pAddress, str.pAddress);return tempString;
}
MyString MyString::operator+(const char* str){
if (str == NULL || strlen(str) == 0){
return *this;
}
MyString tempString;tempString.mLength = this->mLength + strlen(str);
tempString.pAddress = new char[tempString.mLength + 1];
for (int i = 0; i < tempString.mLength + 1;i ++){tempString.pAddress[i] = '\0';
}
strcat(tempString.pAddress, this->pAddress);
strcat(tempString.pAddress, str);return tempString;
}
// , != 重载
bool MyString::operator(const char* p){
if (p == NULL){
return false;
}
if (strcmp(this->pAddress,p) == 0){
return true;
}
return false;
}
bool MyString::operator!=(const char* p){
if (p == NULL){
return false;
}
if (strcmp(this->pAddress, p) != 0){
return true;
}
return false;
}
bool MyString::operator==(MyString& obj){
if (strcmp(this->pAddress, obj.pAddress) == 0){return true;
}
return false;
}
bool MyString::operator!=(MyString& obj){
if (strcmp(this->pAddress, obj.pAddress) != 0){
return true;
}
return false;
}
TestMyString.cpp
//1. 测试+=
void test01(){
MyString str1(“bbb”);
MyString str2(“aaa”);
str1 += str2;
cout << "str1:" << str1 << endl;
str1 += "hello world!";
cout << "str1:" << str1 << endl;
}
//2. 测试+
void test02(){
MyString str1("bbb");
MyString str2("aaa");MyString str3 = str1 + str2; //有问题
cout << "str1:" << str1 << endl;
cout << "str2:" << str2 << endl;
cout << "str3:" << str3 << endl;
cout << "-----------------" << endl;
MyString str4 = str1 + "hello world!";
cout << "str1:" << str1 << endl;
cout << "str4:" << str4 << endl;
}
//3. 测试=、[]
void test03(){
MyString str1(“bbb”);
MyString str2(“aaa”);
cout << “str1:” << str1 << endl;
cout << “str2:” << str2 << endl;
cout << “---------------” << endl;
str1 = str2;
cout << “str1:” << str1 << endl;
cout << “str2:” << str2 << endl;
cout << “---------------” << endl;
cout << “[]:”;
for (int i = 0; i < str1.length(); i++){
cout << str1[i];
}
cout << endl;
}
//4. 测试==、!=
void test04(){
MyString str1("bbb");
MyString str2("aaa");
if (str1 != str2){cout << "不相等!" << endl;
}
if (str1 != "ccc"){cout << "不相等!" << endl;
}str2 = str1;
if (str1 == str2){cout << "相等!" << endl;
}
if (str1 == "bbb"){cout << "相等!" << endl;
}
}
//5. 拷贝构造、=
void test05(){
MyString str1(“bbb”);
MyString str2 = str1;
MyString str3(str1);
cout << "str1:" << str1 << endl;
cout << "str2:" << str2 << endl;
cout << "str3:" << str3 << endl;
}
//6. 右移运算符
void test06(){
MyString str;
"hello world" >> str;cout << "str:" << str << endl;
}
9 附录:运算符和结合性
优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下标 数组名[常量表达式] 左到右 –
() 圆括号 (表达式)/函数名(形参表) –
. 成员选择(对象) 对象.成员名 –
-> 成员选择(指针) 对象指针->成员名 –
- 2 - 负号运算符 -表达式 右到左 单目运算符
- 按位取反运算符 ~表达式
++ 自增运算符 ++变量名/变量名++
– 自减运算符 --变量名/变量名–
* 取值运算符 *指针变量
& 取地址运算符 &变量名
! 逻辑非运算符 !表达式
(类型) 强制类型转换 (数据类型)表达式 --
sizeof 长度运算符 sizeof(表达式) --
3 / 除 表达式/表达式 左到右 双目运算符
* 乘 表达式*表达式
% 余数(取模) 整型表达式%整型表达式
4 + 加 表达式+表达式 左到右 双目运算符
- 减 表达式-表达式
5 << 左移 变量<<表达式 左到右 双目运算符
>> 右移 变量>>表达式
6 > 大于 表达式>表达式 左到右 双目运算符
>= 大于等于 表达式>=表达式
< 小于 表达式<表达式
<= 小于等于 表达式<=表达式
7 == 等于 表达式==表达式 左到右 双目运算符
!= 不等于 表达式!= 表达式
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 | 按位或 表达式|表达式 左到右 双目运算符
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 || 逻辑或 表达式||表达式 左到右 双目运算符
13 ?: 条件运算符 表达式1?
表达式2: 表达式3 右到左 三目运算符
14 = 赋值运算符 变量=表达式 右到左 –
/= 除后赋值 变量/=表达式 –
= 乘后赋值 变量=表达式 –
%= 取模后赋值 变量%=表达式 –
+= 加后赋值 变量+=表达式 –
-= 减后赋值 变量-=表达式 –
<<= 左移后赋值 变量<<=表达式 –
>>= 右移后赋值 变量>>=表达式 –
&= 按位与后赋值 变量&=表达式 –
^= 按位异或后赋值 变量^=表达式 –
|= 按位或后赋值 变量|=表达式 –
15 , 逗号运算符 表达式,表达式,… 左到右 –
五. 继承和派生
1 继承概述
继承是面向对象编程(OOP)的三大特性之一(封装、继承、多态),它允许创建一个新类(派生类)基于一个已存在的类(基类),从而获得基类的属性和方法。
1.1 为什么需要继承
网页类
class IndexPage{
public://网页头部void Header(){cout << "网页头部!" << endl;}//网页左侧菜单void LeftNavigation(){cout << "左侧导航菜单!" << endl;}//网页主体部分void MainBody(){cout << "首页网页主题内容!" << endl;}//网页底部void Footer(){cout << "网页底部!" << endl;}
private:string mTitle; //网页标题
};#if 0
//如果不使用继承,那么定义新闻页类,需要重新写一遍已经有的代码
class NewsPage{
public://网页头部void Header(){cout << "网页头部!" << endl;}//网页左侧菜单void LeftNavigation(){cout << "左侧导航菜单!" << endl;}//网页主体部分void MainBody(){cout << "新闻网页主体内容!" << endl;}//网页底部void Footer(){cout << "网页底部!" << endl;}
private:string mTitle; //网页标题
};void test(){NewsPage* newspage = new NewsPage;newspage->Header();newspage->MainBody();newspage->LeftNavigation();newspage->Footer();
}
#else
//使用继承,可以复用已有的代码,新闻业除了主体部分不一样,其他都是一样的
class NewsPage : public IndexPage{
public://网页主体部分void MainBody(){cout << "新闻网页主主体内容!" << endl;}
};
void test(){NewsPage* newspage = new NewsPage;newspage->Header();newspage->MainBody();newspage->LeftNavigation();newspage->Footer();
}
#endif
int main(){test();return EXIT_SUCCESS;
}
1.2 继承基本概念
c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
派生类中的成员,包含两大部分:
- 一类是从基类继承过来的,一类是自己增加的成员。
- 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
1.3 派生类定义
派生类定义格式:
Class 派生类名 : 继承方式 基类名{//派生类新增的数据成员和成员函数}
三种继承方式:
- public : 公有继承
- private : 私有继承
- protected : 保护继承
从继承源上分:
- 单继承:指每个派生类只直接继承了一个基类的特征
- 多继承:指多个基类 派生出一个派生类的继承关系,多继承的派生类直接继承了不止一 个基类的特征
2 派生类访问控制
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
派生类的访问权限规则如下:
//基类
class A{
public:int mA;
protected:int mB;
private:int mC;
};//1. 公有(public)继承
class B : public A{
public:void PrintB(){cout << mA << endl; //可访问基类public属性cout << mB << endl; //可访问基类protected属性//cout << mC << endl; //不可访问基类private属性}
};
class SubB : public B{void PrintSubB(){cout << mA << endl; //可访问基类public属性cout << mB << endl; //可访问基类protected属性//cout << mC << endl; //不可访问基类private属性}
};
void test01(){B b;cout << b.mA << endl; //可访问基类public属性//cout << b.mB << endl; //不可访问基类protected属性//cout << b.mC << endl; //不可访问基类private属性
}//2. 私有(private)继承
class C : private A{
public:void PrintC(){cout << mA << endl; //可访问基类public属性cout << mB << endl; //可访问基类protected属性//cout << mC << endl; //不可访问基类private属性}
};
class SubC : public C{void PrintSubC(){//cout << mA << endl; //不可访问基类public属性//cout << mB << endl; //不可访问基类protected属性//cout << mC << endl; //不可访问基类private属性}
};
void test02(){C c;//cout << c.mA << endl; //不可访问基类public属性//cout << c.mB << endl; //不可访问基类protected属性//cout << c.mC << endl; //不可访问基类private属性
}
//3. 保护(protected)继承
class D : protected A{
public:void PrintD(){cout << mA << endl; //可访问基类public属性cout << mB << endl; //可访问基类protected属性//cout << mC << endl; //不可访问基类private属性}
};
class SubD : public D{void PrintD(){cout << mA << endl; //可访问基类public属性cout << mB << endl; //可访问基类protected属性//cout << mC << endl; //不可访问基类private属性}
};
void test03(){D d;//cout << d.mA << endl; //不可访问基类public属性//cout << d.mB << endl; //不可访问基类protected属性//cout << d.mC << endl; //不可访问基类private属性
}
3 继承中的构造和析构
3.1 继承中的对象模型
在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:
class Aclass{
public:int mA;int mB;
};
class Bclass : public Aclass{
public:int mC;
};
class Cclass : public Bclass{
public:int mD;
};
void test(){cout << "A size:" << sizeof(Aclass) << endl;cout << "B size:" << sizeof(Bclass) << endl;cout << "C size:" << sizeof(Cclass) << endl;
}
查看类继承的内部模型:
找到VS2013开发人员命令提示程序(一般在:C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts),打开,然后复制你工程路径,命令:cd 路径,进入你工程文件夹中(如果工程不在C盘在E盘的话,要再E:下),然后命令:cl /d1 reportSingleClassLayout类名 文件名全称
如:cl /d1 reportSingleClassLayoutSon test.cpp
3.2 对象构造和析构的调用原则
继承中的构造和析构
- 子类对象在创建时会首先调用父类的构造函数
- 父类构造函数执行完毕后,才会调用子类的构造函数
- 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
- 析构函数调用顺序和构造函数相反
class A{
public:A(){cout << "A类构造函数!" << endl;}~A(){cout << "A类析构函数!" << endl;}
};class B : public A{
public:B(){cout << "B类构造函数!" << endl;}~B(){cout << "B类析构函数!" << endl;}
};class C : public B{
public:C(){cout << "C类构造函数!" << endl;}~C(){cout << "C类析构函数!" << endl;}
};void test(){C c;
}
继承与组合混搭的构造和析构
class D{
public:D(){cout << "D类构造函数!" << endl;}~D(){cout << "D类析构函数!" << endl;}
};
class A{
public:A(){cout << "A类构造函数!" << endl;}~A(){cout << "A类析构函数!" << endl;}
};
class B : public A{
public:B(){cout << "B类构造函数!" << endl;}~B(){cout << "B类析构函数!" << endl;}
};
class C : public B{
public:C(){cout << "C类构造函数!" << endl;}~C(){cout << "C类析构函数!" << endl;}
public:D c;
};
void test(){C c;
}
4.继承中同名成员的处理方法
- 当子类成员和父类成员同名时,子类依然从父类继承同名成员
- 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
- 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
class Base{
public:Base():mParam(0){}void Print(){ cout << mParam << endl; }
public:int mParam;
};class Derived : public Base{
public:Derived():mParam(10){}void Print(){//在派生类中使用和基类的同名成员,显示使用类名限定符cout << Base::mParam << endl;cout << mParam << endl;}//返回基类重名成员int& getBaseParam(){ return Base::mParam; }
public:int mParam;
};int main(){Derived derived;//派生类和基类成员属性重名,子类访问成员默认是子类成员cout << derived.mParam << endl; //10derived.Print();//类外如何获得基类重名成员属性derived.getBaseParam() = 100;cout << "Base:mParam:" << derived.getBaseParam() << endl;return EXIT_SUCCESS;
}
注意: 如果重新定义了基类中的重载函数,将会发生什么?
class Base{
public://重载函数void func1(){cout << "Base::void func1()" << endl;};void func1(int param){cout << "Base::void func1(int param)" << endl;}//非重载函数void myfunc(){cout << "Base::void myfunc()" << endl;}
};class Derived1 : public Base{};
class Derived2 : public Base{
public:void myfunc(){//基类myfunc被隐藏,可通过类作用域运算符指定调用基类myfunc函数//Base::myfunc();cout << "Derived2::void myfunc()" << endl;}
};
class Derived3 : public Base{
public://改变成员函数的参数列表void func1(int param1, int param2){//Base::func1(10); //类的内部可通过类作用域运算符访问基类重载版本的函数cout << "Derived3::void func1(int param1,int param2)" << endl;};
};
class Derived4 : public Base{
public://改变成员函数的返回值int func1(int param){Base::func1(10);cout << "Derived4::int func1(int param)" << endl;return 0;}
};//和基类非重载函数重名
void test01(){Derived1 derived1;derived1.myfunc();//和基类函数重名Derived2 derived2;derived2.myfunc();
}//和基类重载函数重名
void test02(){Derived3 derived3;//derived3.func1(); //基类重载版本的函数fun1被全部隐藏,子类外部不可访问//derived3.func1(10);derived3.func1(10,20);Derived4 derived4;//derived4.func1(); //基类重载版本的函数fun1被全部隐藏,子类外部不可访问derived4.func1(10);
}
//结论:任何时候重新定义基类中的任何一个函数,子类中这种函数的任何版本都会被隐藏(非覆盖,可通过类作用域运算符调用)
任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.
5. 非自动继承的函数
不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。
6. 继承中的静态成员特性
静态成员函数和非静态成员函数的共同点:
- 1.他们都可以被继承到派生类中。
- 2.如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
- 3.如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
静态成员函数不能是虚函数(virtual function).
class Base{
public:static int getNum(){ return sNum; }static int getNum(int param){return sNum + param;}
public:static int sNum;
};
int Base::sNum = 10;class Derived : public Base{
public:static int sNum; //基类静态成员属性将被隐藏
#if 0//重定义一个函数,基类中重载的函数被隐藏static int getNum(int param1, int param2){return sNum + param1 + param2;}
#else//改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数static void getNum(int param1, int param2){cout << sNum + param1 + param2 << endl;}
#endif
};
int Derived::sNum = 20;
7. 多继承
1 多继承概念
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
class Base1{
public:void func1(){ cout << "Base1::func1" << endl; }
};
class Base2{
public:void func1(){ cout << "Base2::func1" << endl; }void func2(){ cout << "Base2::func2" << endl; }
};
//派生类继承Base1、Base2
class Derived : public Base1, public Base2{};
int main(){Derived derived;//func1是从Base1继承来的还是从Base2继承来的?//derived.func1(); derived.func2();//解决歧义:显示指定调用那个基类的func1derived.Base1::func1(); derived.Base2::func1();return EXIT_SUCCESS;
}
多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
解决方法就是显示指定调用那个基类的版本。
2 菱形继承和虚继承
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:
1.羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
2.草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{
public:BigBase(){ mParam = 0; }void func(){ cout << "BigBase::func" << endl; }
public:int mParam;
};class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};int main(){Derived derived;//1. 对“func”的访问不明确//derived.func();//cout << derived.mParam << endl;cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;//2. 重复继承cout << "Derived size:" << sizeof(Derived) << endl; //8return EXIT_SUCCESS;
}
上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,
那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类
。那么我们采用虚基类方式将代码修改如下:
class BigBase{
public:BigBase(){ mParam = 0; }void func(){ cout << "BigBase::func" << endl; }
public:int mParam;
};class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
class Derived : public Base1, public Base2{};int main(){Derived derived;//二义性问题解决derived.func();cout << derived.mParam << endl;//输出结果:12cout << "Derived size:" << sizeof(Derived) << endl;return EXIT_SUCCESS;
}
以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。=
通过虚继承解决了菱形继承所带来的二义性问题。
但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?
3 虚继承实现原理
class BigBase{
public:BigBase(){ mParam = 0; }void func(){ cout << "BigBase::func" << endl; }
public: int mParam;
};
#if 0 //虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
#else //普通继承
class Base1 : public BigBase{};
class Base2 : public BigBase{};
#endif
class Derived : public Base1, public Base2{};
普通继承 虚继承
通过对象布局图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。
- BigBase 菱形最顶层的类,内存布局图没有发生改变。
- Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
- Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。
由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 、Derived三个类对象共享了一份BigBase数据。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
class BigBase{
public:BigBase(int x){mParam = x;}void func(){cout << "BigBase::func" << endl;}
public:int mParam;
};
class Base1 : virtual public BigBase{
public:Base1() :BigBase(10){} //不调用BigBase构造
};
class Base2 : virtual public BigBase{
public:Base2() :BigBase(10){} //不调用BigBase构造
};class Derived : public Base1, public Base2{
public:Derived() :BigBase(10){} //调用BigBase构造
};
//每一次继承子类中都必须书写初始化语句
int main(){Derived derived;return EXIT_SUCCESS;
}
注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的.
工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。
Jerry Schwarz,输入输出流(iostream)的作者,曾在个别场合表示如何他重新设计iostream的话,很可能从iostream中去除多重继承。
六 多态
1 多态基本概念
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
c++支持编译时多态(静态多态) 和 运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
//计算器
class Caculator{
public:void setA(int a){this->mA = a;}void setB(int b){this->mB = b;}void setOperator(string oper){this->mOperator = oper;}int getResult(){if (this->mOperator == "+"){return mA + mB;}else if (this->mOperator == "-"){return mA - mB;}else if (this->mOperator == "*"){return mA * mB;}else if (this->mOperator == "/"){return mA / mB;}}
private:int mA;int mB;string mOperator;
};
//这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改
//面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)
//抽象基类
class AbstractCaculator{
public:void setA(int a){this->mA = a;}virtual void setB(int b){this->mB = b;}virtual int getResult() = 0;
protected:int mA;int mB;
};//加法计算器
class PlusCaculator : public AbstractCaculator{
public:virtual int getResult(){return mA + mB;}
};//减法计算器
class MinusCaculator : public AbstractCaculator{
public:virtual int getResult(){return mA - mB;}
};//乘法计算器
class MultipliesCaculator : public AbstractCaculator{
public:virtual int getResult(){return mA * mB;}
};void DoBussiness(AbstractCaculator* caculator){int a = 10;int b = 20;caculator->setA(a);caculator->setB(b);cout << "计算结果:" << caculator->getResult() << endl;delete caculator;
}
2 向上类型转换及问题
2.1 问题抛出
对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
class Animal{
public:void speak(){cout << "动物在唱歌..." << endl;}
};class Dog : public Animal{
public:void speak(){cout << "小狗在唱歌..." << endl;}
};void DoBussiness(Animal& animal){animal.speak();
}void test(){Dog dog;DoBussiness(dog);
}
运行结果: 动物在唱歌
问题抛出: 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak。
2.2 问题解决思路
解决这个问题,我们需要了解下绑定(捆绑,binding)概念。
- 把函数体与函数调用相联系称为绑定(捆绑,binding)
当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。
上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。
C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。
2.3 问题解决方案(虚函数,vitual function)
C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。
对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.
- 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
- 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
- 在派生类中virtual函数的重定义称为重写(override).
- Virtual关键字只能修饰成员函数.
- 构造函数不能为虚函数
注意:
仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。
class Animal{
public:virtual void speak(){cout << "动物在唱歌..." << endl;}
};
class Dog : public Animal{
public:virtual void speak(){cout << "小狗在唱歌..." << endl;}
};
void DoBussiness(Animal& animal){animal.speak();
}
void test(){Dog dog;DoBussiness(dog);
}
3 C++如何实现动态绑定
动态绑定什么时候发生?
所有的工作都是由编译器在幕后完成。当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求,不会再执行早绑定。
问题:C++的动态捆绑机制是怎么样的?
首先,我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
验证对象中的虚指针:
class A{
public:virtual void func1(){}virtual void func2(){}
};//B类为空,那么大小应该是1字节,实际情况是这样吗?
class B : public A{};void test(){cout << "A size:" << sizeof(A) << endl;cout << "B size:" << sizeof(B) << endl;
}
在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。 编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。
起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。
当子类无重写基类虚函数时:
过程分析:Animal* animal = new Dog;animal->fun1();当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,
此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是
基类的func1.
代码验证:示例代码\71 验证子类无重写基类函数
执行结果: 我是基类的func1
测试结论: 无重写基类的虚函数,无意义
当子类重写基类虚函数时:
过程分析:Animal* animal = new Dog;animal->fun1();当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,
由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1.
代码验证: 示例代码\72 验证子类重写基类函数
执行结果: 我是子类的func1
测试结论: 无重写基类的虚函数,无意义
多态的成立条件:
- 有继承
- 子类重写父类虚函数函数
-
a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
-
b) 子类中virtual关键字可写可不写,建议写
-
- 类型兼容,父类指针,父类引用 指向 子类对象
4 抽象基类和纯虚函数
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function), 使得基类称为抽象类(abstract class).
- 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
- 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
- Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。
建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类.
案例: 模板方法模式
//抽象制作饮品
class AbstractDrinking{
public://烧水virtual void Boil() = 0;//冲泡virtual void Brew() = 0;//倒入杯中virtual void PourInCup() = 0;//加入辅料virtual void PutSomething() = 0;//规定流程void MakeDrink(){Boil();Brew();PourInCup();PutSomething();}
};//制作咖啡
class Coffee : public AbstractDrinking{
public://烧水virtual void Boil(){cout << "煮农夫山泉!" << endl;}//冲泡virtual void Brew(){cout << "冲泡咖啡!" << endl;}//倒入杯中virtual void PourInCup(){cout << "将咖啡倒入杯中!" << endl;}//加入辅料virtual void PutSomething(){cout << "加入牛奶!" << endl;}
};//制作茶水
class Tea : public AbstractDrinking{
public://烧水virtual void Boil(){cout << "煮自来水!" << endl;}//冲泡virtual void Brew(){cout << "冲泡茶叶!" << endl;}//倒入杯中virtual void PourInCup(){cout << "将茶水倒入杯中!" << endl;}//加入辅料virtual void PutSomething(){cout << "加入食盐!" << endl;}
};//业务函数
void DoBussiness(AbstractDrinking* drink){drink->MakeDrink();delete drink;
}void test(){DoBussiness(new Coffee);cout << "--------------" << endl;DoBussiness(new Tea);
}
5 纯虚函数和多继承
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。
绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
注意:除了析构函数外,其他声明都是纯虚函数。
6 虚析构函数
6.1 虚析构函数作用
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class People{
public:People(){cout << "构造函数 People!" << endl;}virtual void showName() = 0;virtual ~People(){cout << "析构函数 People!" << endl;}
};class Worker : public People{
public:Worker(){cout << "构造函数 Worker!" << endl;pName = new char[10];}virtual void showName(){cout << "打印子类的名字!" << endl;}~Worker(){cout << "析构函数 Worker!" << endl;if (pName != NULL){delete pName;}}
private:char* pName;
};void test(){People* people = new Worker;people->~People();
}
6.2 纯虚析构函数
纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。
那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢?
纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。
//非纯虚析构函数
class A{
public:virtual ~A();
};A::~A(){}
//纯析构函数
class B{
public:virtual ~B() = 0;
};B::~B(){}
void test(){A a; //A类不是抽象类,可以实例化对象B b; //B类是抽象类,不可以实例化对象
}
如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。
七. 重写 重载 重定义
重载,同一作用域的同名函数
1.同一个作用域2.参数个数,参数顺序,参数类型不同3.和函数返回值,没有关系4.const也可以作为重载条件 //do(const Teacher& t){} do(Teacher& t)
重定义(隐藏)
1.有继承2.子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
重写(覆盖)
1.有继承2.子类(派生类)重写父类(基类)的virtual函数3.函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class A{
public://同一作用域下,func1函数重载void func1(){}void func1(int a){}void func1(int a,int b){}void func2(){}virtual void func3(){}
};
class B : public A{
public://重定义基类的func2,隐藏了基类的func2方法void func2(){}//重写基类的func3函数,也可以覆盖基类func3virtual void func3(){}
};