C++类和对象学习笔记3

运算符重载

1.加号运算符重载

作用:实现两个自定义数据类型相加的运算

在c++中我们可以通过运算符重载来实现对象的相加,我们如果创建了两个对象p1和p2,我们直接p1+p2,编译器是会报错的,这时我们可以通过自己写成员函数或全局函数来实现两个对象属性相加。这个函数编译器给出了一个统一的名称operator。接下来我们通过一段简单的代码来演示一下这个功能。

#include<iostream>
using namespace std;
class Person
{
public:
	int m_A;
	int m_B;
	Person operator+(Person& p)
	{
		Person tem;
		tem.m_A = this->m_A + p.m_A;
		tem.m_B = this->m_B + p.m_B;
		return tem;
	}
};
void test1()
{
	Person p1;
	Person p2;
	p1.m_A = 10;
	p1.m_B = 20;
	p2.m_A = 15;
	p2.m_B = 25;
	Person p3 = p1 + p2;
	cout << p3.m_A << endl;
	cout << p3.m_B << endl;
}
int main()
{
	test1();
	
	system("pause");
	return 0;
}

其实呢这里其实Person p3=p1+p2这行代码是Persaon p3=p1.operator+(p2)简化而来的,这里其实根自定义函数很像,只不过这里可以简写,operator+就是这个函数的函数名。同理我们再利用全局函数来自定义加号运算符。

#include<iostream>
using namespace std;
class Person
{
public:
	int m_A;
	int m_B;
	
};
Person operator+(Person& p1, Person& p2)
{
	Person tem;
	tem.m_A = p1.m_A + p2.m_A;
	tem.m_B= p1.m_B + p2.m_B;
	return tem;
}
void test1()
{
	Person p1;
	Person p2;
	p1.m_A = 10;
	p1.m_B = 20;
	p2.m_A = 15;
	p2.m_B = 25;
	Person p3 = p1 + p2;
	cout << p3.m_A << endl;
	cout << p3.m_B << endl;
}
int main()
{
	test1();
	
	system("pause");
	return 0;
}

这里跟上面的成员函数来重载加号运算符其实是差不多的,只不过所在区域不同。当然这里重载加号运算符不止可以实现对象的相加也可以实现对象和整形的相加,等等······

2.左移运算符重载

作用:可以输出自定义数据类型

我们先创建一个对象Person p1,如果再编译器上直接写cout<<p1;这时候编译器就会报错,这时候我们就可以重载一下左移运算符。

#include<iostream>
using namespace std;
class Person
{
public:
	Person(int a, int b)
	{
		m_A = a;
		m_B = b;
	}
	int m_A;
	int m_B;
	
};
ostream &operator<<(ostream &cout, Person& p)
{
	cout << "m_A=" << p.m_A << " m_B=" << p.m_B;
	return cout;
}
void test1()
{
	Person p1(10, 20);
	cout << p1 << endl;
}
int main()
{
	test1();
	
	system("pause");
	return 0;
}

在 这里我们要实现左移运算符的重载只能通过编写全局函数来实现,如果我们在类中去编写这个函数,我们无法得到我们想要的简化结果,当然我们根据上面的加号运算符重载可以在类内类似写出一个重载,但是只能得到p<<cout,所以我们只能写全局函数来解决这个问题。我们编写了一个返回类型为ostream的函数,这是cout的数据类型,我们可以通过选择cout右键查看速览定义来看它的数据类型。

这里跟写一个函数很像,括号内传入两个参数然后进行一系列操作,不同的地方可能就是这个函数名是编译器起的而且可以简写。之所以要在函数名前加&是因为在编译器里ostream只有一个,只能通过引用的方式调用。返回ostream类型其实是因为运用了链式编程思维,可以在cout<<p后面加<<其他的输出。

3.递增运算符重载

作用:通过重载递增运算符,实现自己的整形数据

在重载递增运算符之前我们要来了解一下递增运算符++,前置++和后置++是不一样的,我们可以通过输出int a=0来分辨,cout<<a++;这里会输出0,而如果改写成cout<<++a;这里输出的就是1,简而言之,前置++会在输出前完成++这个操作,而后置++会在输出后才来实现这个++操作。

接下来我们就可以根据我们上面所学的来重载++

#include<iostream>
using namespace std;
class Myclass
{
	friend ostream& operator<<(ostream& cout, Myclass p);
public:
	int m_a;
	Myclass(int i)
	{
		m_a = i;
	}
	Myclass& operator++()//前置++
	{
		m_a++;
		return *this;
	}
	Myclass operator++(int)//后置++
	{
		Myclass tem = *this;
		m_a++;
		return tem;
	}
};
ostream& operator<<(ostream& cout, Myclass p)
{
	cout << "m_a=" << p.m_a;
	return cout;
}
int main()
{
	Myclass obj1(0);
	Myclass obj2(0);
	cout << ++(++obj2) << endl;
	cout << obj1++ << endl;
	system("pause");
	return 0;
}

这里我们重载了前置++和后置++,后置++我们返回的是一个副本tem,所以我们无法实现(obj1++)++这种操作,但这种操作是没必要的,因为我们这样不就是(++obj1)++吗,所以没必要,这里注意的点主要是前置++函数括号里为空,而后置++函数括号里为int,这里int主要起占字节的作用,这里编译器自动会认为括号里为int的是后置++,这是编译器默认的,这里相当于函数重载,将前置++和后置++区分开来。

4.赋值运算符重载

在c++中,编译器至少给一个类添加4个函数,分别是无参构造函数,析构函数,默认拷贝构造函数,赋值运算符operator=,对属性进行值拷贝(如果类中涉及属性指向堆区,做赋值操作时也会出现深浅拷贝的问题,具体可以查看前面深浅拷贝的笔记)

在类内编译器会给我们一个默认的operator=重载等号,但是这里给的是值传递,如果在类内int*p,在创建两个对象class1和class2,再class1=class2,他们的p的地址是相同的。所以我们在堆区开辟空间时就需要自己重载赋值运算符。下面我们演示一下用法。

#include<iostream>
using namespace std;
class Person
{
public:
	int* a;
	int b;
	Person(int i, int j)
	{
		a =new int ( i);
		b = j;
	}
	Person& operator=(Person &p)
	{
		if (a != NULL)
		{
			delete a;
			a = NULL;
		}
		a = new int(*p.a);
		b = p. b;
		return *this;
	}
	~Person()
	{
		if (a != NULL)
		{
			delete a;
			a = NULL;
		}
	}
};
int main()
{
	Person p1(10,20);
	Person p2(19, 18);
	p2 = p1;
	cout << *p1.a << endl;
	cout << *p2.a << endl; 
	cout << p1.b << endl;
	cout << p2.b << endl;
	system("pause");
	return 0;
}

这里遇到的问题和深浅拷贝很像,可以查阅一下前面的笔记。

5.关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行比对操作。

类似上面的重载,下面是演示。

#include<iostream>
using namespace std;
class Person
{
public:
	string a;
	int age;
	Person(string b, int c)
	{
		a = b;
		age = c;
	}
	bool operator==(Person& p)
	{
		if (this->a == p.a && this->age == p.age)
		{
			return true;
		}
		return false;
	}
	bool operator!=(Person& p)
	{
		if (this->a == p.a && this->age == p.age)
		{
			return false;
		}
		return true;
	}
};
int main()
{
	Person p1("Tom", 6);
	Person p2("Tom", 6);
	if (p1 == p2)
	{
		cout << "相等" << endl;
	}
	else
	{
		cout << "不相等" << endl;
	}
	system("pause");
	return 0;
}

6.调用运算符重载

1.函数调用运算符也是能够重载的

2.由于重载后使用方式特别像函数调用所以被称为仿函数

3.仿函数没有固定写法,非常灵活。

#include<iostream>
using namespace std;
class Add
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};
class Print
{
public:
	void operator()(string s)
	{
		cout << s << endl;
	}
};
int main()
{
	Add p;
	int i=p(10, 20);
	Print g;
	g("666");
	cout << i << endl;
	cout << Add()(20, 30) << endl;
	system("pause");
	return 0;
}

这里注意一下可以使用匿名对象,匿名对象的特点就是使用完后就会立刻被销毁。调用运算符还可以实现很多功能,可以自己探索。

继承

在定义一些类时我们发现类于类之间可能会出现一些相同的部分,如果我们写重复的代码,那么写代码的效率就会不高,这时候我们就可以使用继承操作来使相同的部分传递到另一个类之中。

基本语法:class A:继承方式  B, 继承方式 C······

这里的继承方式有三种,public(公有继承),protected(保护继承),private(私有继承)。继承的时候,被继承的类称为父类,继承的类称为子类,如果使用的是public,那么子类从父类继承的属性的权限和父类本身的属性相同,父类中的私有属性在子类中是访问不到的无论用哪种继承方式,如果是保护继承,那么子类从父类中继承的公有属性和保护属性全部变为保护属性,父类中的私有属性依旧访问不到,如果是私有继承,那么子类从父类中继承的公有属性和保护属性全部变为私有属性,父类中的私有属性依旧访问不到。

下面我们演示一下怎么使用继承。

#include<iostream>
using namespace std;
class Animal
{
public:
	int age;
protected:
	int m_a;
private:
	int m_b;
};
class Cat :public Animal
{
public:
	int m_c;
};
int main()
{
	Cat a;
	a.age = 10;
	cout << a.age << endl;
	cout << sizeof(a) << endl;
	system("pause");
	return 0;
}

我们这里就简单的演示了一下,这里虽然我们没办法访问父类的私有属性,但是,还是会从父类中继承这个属性,父类中所有非静态成员属性都会被子类继承下来

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

子类继承父类后,当创建子类对象时,编译器会先创建一个父类对象,然后再创建子类对象,所以父类的构造函数调用会先于子类的构造函数,这个很好理解,得先有父亲,才能有儿子,和前面的拷贝构造的析构函数调用一样,后创建的对象先销毁,先创建的对象后销毁,子类的析构函数调用会先于父类的析构函数

继承同名成员函数处理方式

在子类中如果存在与父类函数同名的函数时,访问子类同名成员,只需要直接访问,访问父类中的成员则需要加作用域。

#include<iostream>
using namespace std;
class Animal
{
public:
	void name()
	{
		cout << "Animal无参函数调用" << endl;
	}
	void name(string s)
	{
		cout << "Animal有参函数调用" << endl;
	}
};
class Cat :public Animal
{
public:
	void name()
	{
		cout << "Cat无参函数调用" << endl;
	}
};
int main()
{
	Cat a;
	a.name();
	a.Animal::name("s");
	system("pause");
	return 0;
}

这里需要注意即使父类和子类的同名成员函数参数不一样,但是还是无法直接通过子类访问到对应的成员函数。

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

和同名成员函数一样,访问父类需要加作用域。由于静态成员的特性,我们访问静态成员时,可以通过类名,所以我们在访问继承同名成员时也可通过类名。

#include<iostream>
using namespace std;
class Animal
{
public:
	static void name()
	{
		cout << "Animal无参函数调用" << endl;
	}
	static void name(string s)
	{
		cout << "Animal有参函数调用" << endl;
	}
};
class Cat :public Animal
{
public:
	static void name()
	{
		cout << "Cat无参函数调用" << endl;
	}
};
int main()
{
	Cat::name();
	Cat::Animal::name();
	
	system("pause");
	return 0;
}

当然这里也可以直接写Animal::name();来访问,但是这样就不是通过子类来访问从父类继承的成员了。

当父类有多个时可能会出现同名成员,这时候需要加作用域来区分

菱形继承

菱形继承概念:

两个派生类继承同一个类

有第四个类继承前面两个派生类

在前面我们知道两个父类拥有相同数据,需要加以作用域区分,但是在实际中我们往往只需要一份数据就可以了,这时候我们可以通过虚继承来解决这个问题。

在继承之前我们只需要加上关键字virtural变为虚继承,加上关键字virtual的类称为虚基类。(后面多态会详细介绍virtual)。

#include<iostream>
using namespace std;
class Animal
{
public:
	int age;
};
class Cat :virtual public Animal
{

	
};
class Dog :virtual public Animal
{

};
class Tiandog :public Cat, public Dog
{

};
int main()
{
	Tiandog a;
	a.age = 10;
	cout << a.age << endl;
	system("pause");
	return 0;
}

使用虚继承之后,这里的age也不需要区分,我们来刨析一下这个操作,实际上虚继承是把继承的属性通过地址运算,指向原来父类中的属性,所以这里对象a中只有一个age属性,实际上虚继承只继承了一个可以指向父类属性的指针。

多态

多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

静态多态:函数重载和运算符重载属于静态多态,复用函数名(之前涉及过)

动态多态:派生类和虚函数实现运行时多态 

静态多态和动态多态区别

静态多态的函数地址早绑定(编译阶段确定函数地址)

动态多态的函数地址晚绑定(运行阶段确定函数地址)

下面我们通过一个演示来了解多态

#include<iostream>
using namespace std;
class Animal
{
public:
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};
class Cat :virtual public Animal
{
public:
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
	
};
void dospeak(Animal& p)
{
	p.speak();

}
int main()
{
	Cat Tom;
	dospeak(Tom);
	system("pause");
	return 0;
}

这里先声明一下,在C++中是允许子类对象转换为父类对象的。多态的使用:父类的指针或者引用指向子类对象。

多态满足条件:1.子类重写父类的虚函数。2.有继承关系

我们在前面提过对象上的函数不属于对象,所以这里的对象如果属性没加关键字virtual占用内存应该为1,但我们加上virtual所占用的字节为4,这是因为这里创建了一个vfptr指针v(virtual),f(function),ptr(pointer),翻译成中文就是虚函数指针。我们在其子类不添加任何属性的话,这里会继承到一个指针,它指向vftable,虚函数表,然后虚函数表内部有Animal::speak()。如果我们按上面演示给子类添加了一个speak的虚函数,那么子类的虚函数就会覆盖父类的,最终我们访问到的就是Cat::speak()。

纯虚函数和抽象类

在多态中,通常父类中虚函数是毫无意义的,主要通过调用子类中重写的内容,因此我们可以将虚函数改为纯虚函数。

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)=0;

当类中有了纯虚函数,这个类也称为抽象类。

抽象类特点:1.无法实例化对象 (这个很好理解,有函数无法执行,所以创建不了)2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

#include<iostream>
using namespace std;
class Animal
{
public:
	virtual void speak() = 0;
	
};
class Cat :public Animal
{
public:
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
	
};
void dospeak(Animal& p)
{
	p.speak();

}
int main()
{
	Cat Tom;
	dospeak(Tom);
	system("pause");
	return 0;
}

虚析构和纯虚析构

多态使用时,如果子类中属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方法:将父类中的析构函数改为虚析构或纯虚析构

虚析构和纯虚析构共性:

1.可以解决父类指针释放子类对象

2.都需要具体的函数实现

虚析构和纯虚析构的区别:纯虚析构的类属于抽象类,无法实例化对象

虚析构语法:virtual ~类名(){}

纯虚析构语法:virtual ~类名=0;                                                                                                       类名::~类名(){}

实例:

#include<iostream>
using namespace std;
#include<string>
class Animal
{
public:
	virtual void speak() = 0;
	/*virtual ~Animal()
	{
		cout << "Animal析构函数调用" << endl;
	}*/
	virtual ~Animal() = 0;//纯虚析构
};
Animal::~Animal()
{
	cout << "Animal析构函数调用" << endl;
}
class Cat :public Animal
{
public:
	Cat(string s)
	{
		name = new string (s);
	}
	virtual void speak()
	{
		cout <<*name<< "小猫在说话" << endl;
	}
	~Cat()
	{
		cout << "Cat析构函数调用" << endl;
		delete name;
		name = NULL;
	}
	string* name;
};

int main()
{
	Animal* animal = new Cat("Tom");
	animal->speak();
	delete animal;
	system("pause");
	return 0;
}

父类指针在析构的时候,不会调用子类中的析构函数,所以我们需要用虚析构或纯虚析构来重写析构函数。我们在堆区开辟了空间时就需要使用虚析构或纯虚析构来处理。

类和对象的学习笔记就到这里结束了!

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值