运算符重载是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
加减乘除运算符的重载是一样的,只需要将operator后面的符号换成相应的运算符即可,下面以加号运算符的重载为例。
成员函数重载+运算符。
#include
#include
using namespace std;
class Person
{
public:
//成员函数重载+运算符
Person operator+(Person &p)
{
Person temp;
temp.a = this->a+p.a;
temp.b = this->b+p.b;
return temp;
}
public:
int a;
int b;
};
int main()
{
Person p1,p2;
p1.a = 10;
p1.b = 20;
p2.a = 10;
p2.b = 20;
Person p3;
p3 = p1 + p2; //相当于 p3 = p1.operator+(p2);
cout<<"p3.a="<<p3.a<<" p3.b="<<p3.b<<endl;
system("pause");
return 0;
}
全局函数重载+运算符。
#include
#include
using namespace std;
class Person
{
public:
int a;
int b;
};
//全局函数重载+运算符
Person operator+(Person &p1,Person &p2)
{
Person temp;
temp.a = p1.a+p2.a;
temp.b = p1.b+p2.b;
return temp;
}
int main()
{
Person p1,p2;
p1.a = 10;
p1.b = 20;
p2.a = 10;
p2.b = 20;
Person p3;
p3 = p1 + p2; //相当于 p3 = operator+(p1,p2);
cout<<"p3.a="<<p3.a<<" p3.b="<<p3.b<<endl;
system("pause");
return 0;
}
重载左移运算符配合友元可以实现自定义数据类型的输出。
左移运算符重载不能在成员函数中实现,因为涉及到传参,cout应在类前面。
转到cout的定义处,其是一个ostream类型的变量。
__PURE_APPDOMAIN_GLOBAL extern istream cin, *_Ptr_cin;
__PURE_APPDOMAIN_GLOBAL extern ostream cout, *_Ptr_cout;
左移运算符重载的代码如下。
#include
#include
using namespace std;
class Person
{
//全局函数作为友元访问私有属性
friend ostream& operator<<(ostream &out,Person &p);
public:
Person(int a,int b)
{
this->a = a;
this->b = b;
}
private:
int a;
int b;
};
//重载左移运算符
ostream& operator<<(ostream &out,Person &p)
{
out<<"a="<<p.a<<" b="<<p.b;
return out;
}
int main()
{
Person p(10,20);
cout<<p<<endl; //直接输出对象,需要重载左移运算符才可以
system("pause");
return 0;
}
前置递增运算返回引用,后置递增运算返回值。
递增运算符重载的代码如下。
#include
#include
using namespace std;
class Person
{
friend ostream& operator<<(ostream &out,Person p);
public:
Person()
{
num = 0;
}
//重载前置++运算符
Person& operator++() //需要返回引用,这样每次递增操作都是对同一个对象
{
num++;
return *this;
}
//重载后置++运算符
Person operator++(int) //占位参数用来区分前置++和后置++
{
Person temp = *this; //先返回以前值,再++,但是返回不能在++之前,因此需要先记录
num++;
return temp;
}
private:
int num;
};
//重载左移运算符
ostream& operator<<(ostream &out,Person p)
{
out<<p.num;
return out;
}
int main()
{
Person p1,p2;
cout<<"1.p1++="<<p1++<<endl;
cout<<"2.p1="<<p1<<endl;
cout<<"1.++p2="<<++p2<<endl;
cout<<"2.p2="<<p2<<endl;
system("pause");
return 0;
}
赋值运算符重载是为了实现涉及堆区内存的拷贝,要以深拷贝的形式实现,浅拷贝的方式在析构函数执行时会发生错误。
赋值运算符重载的代码如下。
#include
#include
using namespace std;
class Person
{
public:
Person(int age)
{
this->age = new int(age);
}
//重载赋值运算符
Person& operator=(Person &p)
{
//age = p.age; //默认的重载内容,浅拷贝
//先判断属性是否在堆区,如果在就先释放,再接收拷贝
if(age != NULL)
{
delete age;
age = NULL;
}
age = new int(*p.age); //深拷贝实现
return *this; //为了实现连等赋值,需要返回自身而不能是void类型
}
//析构函数中释放堆区内存
~Person()
{
if(age != NULL)
{
delete age;
age = NULL;
}
}
int *age;
};
void fun() //需要在函数中测试,这样函数返回后就会调用析构函数
{
Person p1(18);
Person p2(20);
Person p3(22);
p3 = p2 = p1;
cout<<"p1的年龄为:"<<*p1.age<<endl;
cout<<"p2的年龄为:"<<*p2.age<<endl;
cout<<"p3的年龄为:"<<*p3.age<<endl;
}
int main()
{
fun();
system("pause");
return 0;
}
重载两个关系运算符,可以让两个自定义类型的对象进行对比。
关系运算符重载的代码如下。
#include
#include
using namespace std;
class Person
{
public:
Person(string name,int age)
{
this->name = name;
this->age = age;
}
//重载关系运算符
bool operator==(Person &p)
{
if(name == p.name && age == p.age)
return true;
else
return false;
}
private:
string name;
int age;
};
void fun() //需要在函数中测试,这样函数返回后就会调用析构函数
{
Person p1("aaa",18);
Person p2("bbb",20);
Person p3("aaa",18);
if(p1==p2)
cout<<"p1和p2年龄和名字都相同!"<<endl;
else
cout<<"p1和p2不相同!"<<endl;
if(p1==p3)
cout<<"p1和p3年龄和名字都相同!"<<endl;
else
cout<<"p1和p3不相同!"<<endl;
}
int main()
{
fun();
system("pause");
return 0;
}
函数调用运算符是 () ,由于重载后使用的方式像函数调用,因此称为仿函数,仿函数没有固定写法,非常灵活。
函数调用运算符重载的代码如下。
#include
#include
using namespace std;
class Person
{
public:
//重载函数调用运算符
void operator()(string s)
{
cout<<s<<endl;
}
void operator()(int a,int b)
{
cout<<"result = "<<a+b<<endl;
}
};
void fun(string s)
{
cout<<s<<endl;
}
int main()
{
Person p;
p("Hello operator()!"); //重载函数调用运算符后调用
fun("Hello fun()!"); //函数调用
p(100,200);
Person()(10,20); //匿名对象调用
system("pause");
return 0;
}
继承是面向对象的三大特性之一。
类与类之间存在特殊的关系,比如下面的类中,下级的成员除了拥有上一级的共性外,还有自己的特性。
这个时候可以使用继承,减少重复的代码,这也是通过继承带来的好处。
继承的语法:class 子类名 :继承方式(public等) 父类名
继承方式包括公共继承(public) 、保护继承(protected) 和私有继承(private) 。
父类也称基类,子类也称派生类。
派生类中的成员包含两部分,一部分是从基类继承过来的,一类是自己增加的成员,从基类继承过来的表现为共性,子类中新增的成员体现了其个性。
通过公共继承(public) 方式,成员属性或方法在父类中是什么样的访问方式,在子类中还是怎样的访问方式;通过保护继承(protected) 方式,成员属性或方法在父类中是公共或保护访问方式的,在子类中以保护方式访问;通过私有继承(private)方式,成员属性或方法在父类中是公共或保护访问方式的,在子类中以私有方式访问;父类中私有的成员属性或方法在子类中不能被访问。保护权限下的成员属性或方法在子类内可以访问,子类外不可以访问。
如果再对子类做继承,同样要看继承方式,子类中私有的属性或方法经继承后全都访问不了。
值得一提的是,父类中的私有成员只是被隐藏了,但是仍然会继承下去。
网页中一般都是这样设计的,有公共的头部和底部信息,点开不同的链接之后,这些公有的信息没有变化,但是却有自己独有的部分,这就用到了继承。
如果不使用继承,对上面提到的网页例子以普通的方式来实现,代码如下所示。
#include
#include
using namespace std;
class Java
{
public:
void head()
{
cout<<"公共头部信息..."<<endl;
}
void foot()
{
cout<<"公共底部信息..."<<endl;
}
void content()
{
cout<<"Java学习视频..."<<endl;
}
};
class Python
{
public:
void head()
{
cout<<"公共头部信息..."<<endl;
}
void foot()
{
cout<<"公共底部信息..."<<endl;
}
void content()
{
cout<<"Python学习视频..."<<endl;
}
};
class CPP
{
public:
void head()
{
cout<<"公共头部信息..."<<endl;
}
void foot()
{
cout<<"公共底部信息..."<<endl;
}
void content()
{
cout<<"C++学习视频..."<<endl;
}
};
void fun()
{
Java ja;
ja.head();
ja.content();
ja.foot();
cout<<"----------------"<<endl;
Python py;
py.head();
py.content();
py.foot();
cout<<"----------------"<<endl;
CPP c;
c.head();
c.content();
c.foot();
}
int main()
{
fun();
system("pause");
return 0;
}
可以看到,不同的类中用到了许多重复的代码,如果采用继承的方式,优化后的代码如下。
#include
#include
using namespace std;
class BasePage
{
public:
void head()
{
cout<<"公共头部信息..."<<endl;
}
void foot()
{
cout<<"公共底部信息..."<<endl;
}
};
//继承基类,一定要加继承方式public,不写默认是private,无法访问
class Java : public BasePage
{
public:
void content()
{
cout<<"Java学习视频..."<<endl;
}
};
class Python : public BasePage
{
public:
void content()
{
cout<<"Python学习视频..."<<endl;
}
};
class CPP : public BasePage
{
public:
void content()
{
cout<<"C++学习视频..."<<endl;
}
};
void fun()
{
Java ja;
ja.head();
ja.content();
ja.foot();
cout<<"----------------"<<endl;
Python py;
py.head();
py.content();
py.foot();
cout<<"----------------"<<endl;
CPP c;
c.head();
c.content();
c.foot();
}
int main()
{
fun();
system("pause");
return 0;
}
上面的两个代码运行结果是一样的,如下图所示,但是通过继承方式写出来的代码减少了很多的重复,这种重复在多继承的情况的尤为明显。
父类中所有的非静态成员属性都会被子类继承下去,父类中的私有成员属性被编译器隐藏了,但是仍然会被继承。
下面代码很好的说明了父类中的哪些属性被子类继承了。
先执行父类的构造函数,再执行子类的构造函数;析构函数的执行顺序是先执行子类的析构函数,再执行父类的析构函数。
测试构造函数和析构函数的顺序的代码如下。
#include
#include
using namespace std;
class Parent
{
public:
Parent()
{
cout<<"Parent构造函数!"<<endl;
}
~Parent()
{
cout<<"Parent析构函数!"<<endl;
}
};
class Son : public Parent
{
public:
Son()
{
cout<<"Son构造函数!"<<endl;
}
~Son()
{
cout<<"Son析构函数!"<<endl;
}
};
void fun()
{
Son s;
}
int main()
{
fun();
system("pause");
return 0;
}
如果子类有和父类同名的成员,访问子类成员的时候,直接访问即可,访问父类成员则要加父类名限定作用域。
如果子类有和父类同名的成员函数,子类的同名成员会隐藏掉父类中的所有同名成员函数,包括重载的函数。如果要访问到父类中被隐藏的同名成员函数,需要加作用域。
继承中同名成员属性或成员方法的使用和调用如下图所示。
#include
#include
using namespace std;
class Parent
{
public:
Parent()
{
a = 10;
}
void fun()
{
cout<<"Parent类中的fun()函数调用!"<<endl;
}
void fun(int b)
{
cout<<"Parent类中的fun(int b)函数调用!"<<endl;
}
int a;
};
class Son : public Parent
{
public:
Son()
{
a = 20;
}
void fun()
{
cout<<"Son类中的fun()函数调用!"<<endl;
}
int a;
};
void fun()
{
Son s;
cout<<"s.a = "<<s.a<<endl;
cout<<"s.Parent::a = "<<s.Parent::a<<endl;
s.fun();
s.Parent::fun();
s.Parent::fun(1);
}
int main()
{
fun();
system("pause");
return 0;
}
同名静态成员的处理方式和非静态处理方式一样,不过静态成员的访问方式既可以通过对象实现,也可以通过类名实现。
通过类名访问实现时需要注意两个::的区别,前一个::是以类名的方式访问成员,后一个::则是区分同名成员的作用域。
继承中同名静态成员处理方式的代码如下。
#include
#include
using namespace std;
class Parent
{
public:
static void fun()
{
cout<<"Parent类中的fun()函数调用!"<<endl;
}
static void fun(int b)
{
cout<<"Parent类中的fun(int b)函数调用!"<<endl;
}
static int a; //类内声明
};
class Son : public Parent
{
public:
static void fun()
{
cout<<"Son类中的fun()函数调用!"<<endl;
}
static int a;
};
int Parent::a = 10; //类外初始化
int Son::a = 20;
void fun()
{
Son s;
cout<<"通过实例化的对象访问静态成员:"<<endl;
cout<<"s.a = "<<s.a<<endl;
cout<<"s.Parent::a = "<<s.Parent::a<<endl;
s.fun();
s.Parent::fun();
s.Parent::fun(1);
cout<<"通过类访问静态成员:"<<endl;
cout<<"Son::a = "<<Son::a<<endl;
//cout<<"Parent::a = "<
cout<<"Parent::a = "<<Son::Parent::a<<endl;
//第一个::是以类名的方式访问静态成员;第二个::是以限定同名成员变量的作用域
Son::fun();
Son::Parent::fun();
Son::Parent::fun(1);
}
int main()
{
fun();
system("pause");
return 0;
}
C++中允许一个类继承多个类,多继承可能会出现同名成员,需要加作用域进行区分。
多继承语法:class 子类名 :继承方式 父类名1,继承方式 父类名2,…
多继承的代码示例如下所示。
#include
#include
using namespace std;
class Base1
{
public:
Base1()
{
a = 10;
}
int a;
};
class Base2
{
public:
Base2()
{
a = 20;
}
int a;
};
class Son : public Base1, public Base2
{
public:
Son()
{
a = 30;
}
int a;
};
void fun()
{
Son s;
cout<<"s.a = "<<s.a<<endl;
cout<<"s.Base1::a = "<<s.Base1::a<<endl;
cout<<"s.Base2::a = "<<s.Base2::a<<endl;
}
int main()
{
fun();
system("pause");
return 0;
}
上面代码的执行结果如下图所示。
如果不加作用域,默认访问的是本类的成员变量,如果要访问不同类中的同名成员变量,需要加上作用域才能实现访问。
菱形继承:两个子类(派生类)继承了同一个父类(基类),同时又有某个类继承了两个子类,菱形继承也称钻石继承。
菱形继承的一个典型例子如下图所示。
菱形继承中,子类继承父类的时候会将属性在各自的类中继承一份,因此,再有类继承子类就会得到两个相同的属性,所以菱形继承中一定存在着同名成员,导致资源浪费且有出现歧义。要利用虚继承解决同名成员的问题,在继承方式前面加上关键字virtual就代表虚继承。
虚继承继承的不再是成员的属性,而是一个虚基类指针,这两个指针在子类中通过不同的偏移量最终指向父类中的成员属性,如下图所示。
菱形继承的代码示例如下。
#include
#include
using namespace std;
//基类
class Animal
{
public:
int age;
};
//派生类
class Sheep : virtual public Animal{};
class Tuo : virtual public Animal{};
//继承两个派生类
class SheepTuo : public Sheep, public Tuo{};
void fun()
{
SheepTuo st;
st.age = 18;
cout<<"st.age = "<<st.age<<endl;
cout<<"st.Sheep::age = "<<st.Sheep::age<<endl;
cout<<"st.Tuo::age = "<<st.Tuo::age<<endl;
cout<<"st.Animal::age = "<<st.Tuo::age<<endl;
cout<<"sizeof(st) = "<<sizeof(st)<<endl;
}
int main()
{
fun();
system("pause");
return 0;
}
上面代码的执行结果如下图所示。
可以看到,通过虚继承,访问成员属性的时候直接通过.访问即可, 不用再加类名作为作用域,因为继承下来的数据只有一份,不会产生歧义。
最终类的大小是12个字节,其中有4个字节是继承自父类的成员属性,另外8个字节分别是来自两个派生类的虚基类指针。
若不采用虚继承的方式,上面类的大小是8个字节,父类中的成员属性被两个派生类继承了,再由一个类继承了两个派生类的属性,因此就是8字节大小。
本文参考视频:
黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难