从0到1入门C++编程——05 类和对象之运算符重载、继承

文章目录

  • 运算符重载
    • 1.加号运算符重载
    • 2.左移运算符重载
    • 3.递增运算符重载
    • 4.赋值运算符重载
    • 5.关系运算符重载
    • 6.函数调用运算符重载
  • 继承
    • 1.继承的基本语法及继承方式
    • 2.继承中的对象模型
    • 3.继承中构造函数和析构函数的顺序
    • 4.继承中同名成员的处理方式
    • 5.继承中同名静态成员处理方式
    • 6.继承多个父类
    • 7.菱形继承

运算符重载

运算符重载是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

1.加号运算符重载

加减乘除运算符的重载是一样的,只需要将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;
}

2.左移运算符重载

重载左移运算符配合友元可以实现自定义数据类型的输出。
左移运算符重载不能在成员函数中实现,因为涉及到传参,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;
}

上面代码的运行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第1张图片

3.递增运算符重载

前置递增运算返回引用,后置递增运算返回值。
递增运算符重载的代码如下。

#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;
}

上面代码的运行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第2张图片

4.赋值运算符重载

赋值运算符重载是为了实现涉及堆区内存的拷贝,要以深拷贝的形式实现,浅拷贝的方式在析构函数执行时会发生错误。
赋值运算符重载的代码如下。

#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;
}

上面代码的运行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第3张图片

5.关系运算符重载

重载两个关系运算符,可以让两个自定义类型的对象进行对比。
关系运算符重载的代码如下。

#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;
}

上面代码的运行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第4张图片

6.函数调用运算符重载

函数调用运算符是 () ,由于重载后使用的方式像函数调用,因此称为仿函数,仿函数没有固定写法,非常灵活。
函数调用运算符重载的代码如下。

#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;
}

上面代码的运行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第5张图片


继承

1.继承的基本语法及继承方式

继承是面向对象的三大特性之一。
类与类之间存在特殊的关系,比如下面的类中,下级的成员除了拥有上一级的共性外,还有自己的特性。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第6张图片
这个时候可以使用继承,减少重复的代码,这也是通过继承带来的好处。
继承的语法:class 子类名 :继承方式(public等) 父类名
继承方式包括公共继承(public) 、保护继承(protected) 和私有继承(private) 。
父类也称基类,子类也称派生类。
派生类中的成员包含两部分,一部分是从基类继承过来的,一类是自己增加的成员,从基类继承过来的表现为共性,子类中新增的成员体现了其个性。
通过公共继承(public) 方式,成员属性或方法在父类中是什么样的访问方式,在子类中还是怎样的访问方式;通过保护继承(protected) 方式,成员属性或方法在父类中是公共或保护访问方式的,在子类中以保护方式访问;通过私有继承(private)方式,成员属性或方法在父类中是公共或保护访问方式的,在子类中以私有方式访问;父类中私有的成员属性或方法在子类中不能被访问。保护权限下的成员属性或方法在子类内可以访问,子类外不可以访问。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第7张图片
如果再对子类做继承,同样要看继承方式,子类中私有的属性或方法经继承后全都访问不了。
值得一提的是,父类中的私有成员只是被隐藏了,但是仍然会继承下去。
网页中一般都是这样设计的,有公共的头部和底部信息,点开不同的链接之后,这些公有的信息没有变化,但是却有自己独有的部分,这就用到了继承。
如果不使用继承,对上面提到的网页例子以普通的方式来实现,代码如下所示。

#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;
}

上面的两个代码运行结果是一样的,如下图所示,但是通过继承方式写出来的代码减少了很多的重复,这种重复在多继承的情况的尤为明显。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第8张图片

2.继承中的对象模型

父类中所有的非静态成员属性都会被子类继承下去,父类中的私有成员属性被编译器隐藏了,但是仍然会被继承。
下面代码很好的说明了父类中的哪些属性被子类继承了。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第9张图片

3.继承中构造函数和析构函数的顺序

先执行父类的构造函数,再执行子类的构造函数;析构函数的执行顺序是先执行子类的析构函数,再执行父类的析构函数。
测试构造函数和析构函数的顺序的代码如下。

#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;
}

上面代码的执行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第10张图片

4.继承中同名成员的处理方式

如果子类有和父类同名的成员,访问子类成员的时候,直接访问即可,访问父类成员则要加父类名限定作用域。
如果子类有和父类同名的成员函数,子类的同名成员会隐藏掉父类中的所有同名成员函数,包括重载的函数。如果要访问到父类中被隐藏的同名成员函数,需要加作用域。
继承中同名成员属性或成员方法的使用和调用如下图所示。

#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;
}

上面代码的执行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第11张图片

5.继承中同名静态成员处理方式

同名静态成员的处理方式和非静态处理方式一样,不过静态成员的访问方式既可以通过对象实现,也可以通过类名实现。
通过类名访问实现时需要注意两个::的区别,前一个::是以类名的方式访问成员,后一个::则是区分同名成员的作用域。
继承中同名静态成员处理方式的代码如下。

#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;
}

上面代码的执行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第12张图片

6.继承多个父类

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;
}

上面代码的执行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第13张图片
如果不加作用域,默认访问的是本类的成员变量,如果要访问不同类中的同名成员变量,需要加上作用域才能实现访问。

7.菱形继承

菱形继承:两个子类(派生类)继承了同一个父类(基类),同时又有某个类继承了两个子类,菱形继承也称钻石继承。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第14张图片
菱形继承的一个典型例子如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第15张图片
菱形继承中,子类继承父类的时候会将属性在各自的类中继承一份,因此,再有类继承子类就会得到两个相同的属性,所以菱形继承中一定存在着同名成员,导致资源浪费且有出现歧义。要利用虚继承解决同名成员的问题,在继承方式前面加上关键字virtual就代表虚继承。
虚继承继承的不再是成员的属性,而是一个虚基类指针,这两个指针在子类中通过不同的偏移量最终指向父类中的成员属性,如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第16张图片
菱形继承的代码示例如下。

#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;
}

上面代码的执行结果如下图所示。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第17张图片
可以看到,通过虚继承,访问成员属性的时候直接通过.访问即可, 不用再加类名作为作用域,因为继承下来的数据只有一份,不会产生歧义。
最终类的大小是12个字节,其中有4个字节是继承自父类的成员属性,另外8个字节分别是来自两个派生类的虚基类指针。
若不采用虚继承的方式,上面类的大小是8个字节,父类中的成员属性被两个派生类继承了,再由一个类继承了两个派生类的属性,因此就是8字节大小。
从0到1入门C++编程——05 类和对象之运算符重载、继承_第18张图片


本文参考视频:
黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难

你可能感兴趣的:(C++,c++)