文章目录
八、类和对象
C++面向对象的三大特性:封装、继承和多态。
(1)封装
封装的意义在于:
- 将属性和行为作为一个整体;
- 将属性和行为加以权限控制;
- 在设计类的时候,将属性和行为(方法)结合,来表现具体的事物。
基本语法:class 类名{ 访问权限: 属性 / 行为 };
或者class 类名{ 访问权限: 属性 / 行为的声明}; 返回值类型 类名::行为定义 {};
实例化,通过类创建类的实例语法:类名 实例名;
,可以通过==成员访问符.
==来访问类中的属性和方法。示例:
#include <iostream>
using namespace std;
const double Pi = 3.1415;
class Circle { //创建一个圆类
public:
float radius; //属性
double calculateZC() //方法
{
return 2 * Pi * radius;
}
};
int main(void)
{
Circle c1; //实例化
c1.radius = 10.0;
cout << c1.calculateZC() << endl;
system("pause");
return 0;
}
封装的权限设置:类在设计时,可以把属性和方法放在不同的权限下加以控制。访问权限有三种:
public
公共权限,类内可以访问,类外也可以访问,继承的类可以访问公共权限;protected
保护权限,类内可以访问,类外不能访问,继承的类可以访问保护权限;private
私有权限,类内可以访问,类外不能访问,继承的类不能访问私有权限。
在C++中,struct
和class
的区别:唯一的区别在于默认的访问权限不同,struct
默认的访问权限public
;而class
默认访问权限为private
。
将类的成员属性设置为私有:
- 将属性设置为
private
,同时提供public
的方法来控制这些属性;- 可以自己控制读写权限;
- 对于写可以检测数据的有效性。
举个例子:
#include <iostream>
using namespace std;
class Person {
private:
string name; //可读可写
int age; //可读可写
string lover; //只写
public:
void setName(string n)
{
name = n;
}
string getName()
{
return name;
}
void setAge(int n)
{
if (n > 100 or n < 0) //检测输入的有效性
{
cout << "Wrong Age" << endl;
return;
}
age = n;
}
int getAge()
{
return age;
}
void setLover(string n)
{
lover = n;
}
};
int main(void)
{
Person p;
p.setName("Hello");
cout << p.getName() << endl;
p.setAge(20);
cout << p.getAge() << endl;
p.setLover("Alan");
return 0;
}
(2)对象的初始化和清理
对象的初始化和清理是两个非常重要的安全问题,一个对象或者变量没有初始状态,对其使用后果是未知的;同样在使用完一个对象或变量没有及时地清理,也会造成一种安全问题。
1、构造、析构函数
C++使用了构造函数和析构函数解决上述问题,构造函数的目的是对属性进行初始化,析构函数是释放对象前完成清理工作。**默认情况下Cpp的构造和析构函数都是public
类型。**这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要求的,如果我们不提供构造函数和解析函数,编译器会默认这两个函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用;
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名 () {}
1、构造函数,没有返回值也不写
void
;2、函数名称与类名相同;
3、构造函数可以有参数,因此可以发生函数重载;
4、程序在调用对象时会自动调用构造函数,无需手动调用,并且只会调用一次。
析构函数语法:~类名 () {}
1、析构函数,没有返回值也不写
void
;2、函数名称与类名相同,在名称前面加上==
~
==;3、析构函数不可以有参数,因此不能发生函数重载;
4、程序在对象销毁前会自动调用析构函数,无需手动调用,并且只会调用一次。
一个简单的示例:
#include <iostream>
using namespace std;
class Person {
public:
Person() //构造函数,在创建类实例的时候执行
{
cout << "Person 构造函数的调用" << endl;
}
~Person() //析构函数,在类实例销毁的时候执行
{
cout << "Person 析构函数的调用" << endl;
}
};
void test01()
{
Person p;
}
int main(void)
{
test01();
Person p;
system("pause");
return 0;
}
构造函数的分类及调用:构造函数有两种分类方式,
- 按照参数分为,有参构造和无参构造(默认构造);
- 按照类型分为,普通构造和拷贝构造。
无参构造即默认构造,当构造函数中不需要参数的时就称为无参构造;有参构造是通过构造函数来对类的一些属性进行初始化。需要注意:拷贝构造需要传递自身相应的类型,并且以引用的方式传递,还需要加const
修饰防止对源数据进行修改,语法==类名 (const 类名& 形参)
==。
构造函数有三种调用方式,
- 分别是括号法,调用语法
类名 实例名 (参数/另一个实例)
,括号法调用默认构造的时候,不需要()
,如果使用了()
编译器会认为这是一个函数的声明;- 显示法,调用语法
类名 实例名 = 类名(参数/另一个实例)
- 隐式转换法,调用语法
类名 实例名 = 参数/另一个实例
#include <iostream>
using namespace std;
class Person {
public:
Person() //构造函数
{
cout << "Person 构造函数的调用" << endl;
}
Person(int a) //有参构造函数
{
age = a;
cout << "Person 有参构造函数的调用" << endl;
}
Person(const Person& p)
{
age = p.age;
cout << "Person 拷贝构造函数的调用" << endl;
}
~Person() //析构函数
{
cout << "Person 析构函数的调用" << endl;
}
int age; //年龄属性
};
void test01()
{
//1、括号法
Person p1; //调用默认构造
Person p2(10); //普通有参构造函数
Person p3(p1); //拷贝构造函数
//2、显示法
Person p4 = Person(20); //普通有参构造函数
Person p5 = Person(p2); //拷贝构造函数
//3、隐式转换法
Person p6 = 10; //普通有参构造函数
Person p7 = p2; //拷贝构造函数
}
int main(void)
{
test01();
system("pause");
return 0;
}
什么时候使用拷贝构造函数:
- 使用一个已经创建完毕的对象来初始化另一个新对象;
- 值传递的方式给函数参数传值,当实例对象作为函数的实参的时候,是通过值传递的方式本质上是一种拷贝;
- 以值方式返回局部对象,当实例对象作为函数返回值的时候,也是通过值传递的方式,本质上也是一种拷贝,此时会创建一个实例对象的副本。
一个示例:
#include <iostream>
using namespace std;
class Person {
public:
Person()
{
cout << "Person 默认构造函数调用" << endl;
}
Person(int age)
{
m_Age = age;
cout << "Person 普通参数构造函数调用" << endl;
}
Person(const Person& p)
{
m_Age = p.m_Age;
cout << "Person 拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person 析构函数调用" << endl;
}
int m_Age;
};
void test01()
{
Person p1(10);
Person p2(p1);
}
void test02(Person p)
{
}
Person test03()
{
Person p;
return p;
}
int main(void)
{
//1、使用一个已经创建的对象来初始化另一个对象
//test01();
//2、值传递的方式传递函数参数
//Person per(10);
//test02(per);
//3、以值返回局部对象
Person per2 = test03();
return 0;
}
2、深浅拷贝
C++构造函数调用规则:默认情况下,C++编译器至少给一个类添加三个函数
1、默认构造函数(无参、函数体为空);
2、默认析构函数(无参、函数体为空);
3、默认拷贝构造函数,对所有属性进行值拷贝。
4、如果用户定义有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造函数;
5、如果用户定义拷贝构造函数,C++不会再提供其他的构造函数。
深拷贝和浅拷贝:
- 浅拷贝:简单的赋值拷贝操作,浅拷贝会带来问题,让堆区的内存重复释放;
- 深拷贝:在堆区重新申请空间,进行拷贝操作。
总结,如果属性有在堆区进行开辟的,一定要自己提供拷贝构造函数,防止浅拷贝的问题。举一个示例:
#include <iostream>
using namespace std;
class Person {
public:
int m_Age;
int *m_Height; //指向身高的指针
Person()
{
cout << "Person 的默认构造函数" << endl;
}
Person(int age, int height)
{
m_Age = age;
m_Height = new int(height);
cout << "Person 的有参构造函数" << endl;
}
Person(const Person& p)
{
m_Age = p.m_Age;
m_Height = new int(*p.m_Height); //采用深拷贝,手动分配空间
cout << "Person 的拷贝构造函数 " << endl;
}
~Person()
{
if (m_Height != NULL)
{
delete m_Height;
m_Height = NULL;
}
cout << "Person 的析构函数" << endl;
}
};
void test01()
{
Person p1(22, 183);
cout << "p1 age is " << p1.m_Age << " height is " << *p1.m_Height << endl;
Person p2(p1);
cout << "p2 age is " << p2.m_Age << " height is " << *p2.m_Height << endl;
}
int main(void)
{
test01();
return 0;
}
构造函数初始化列表:C++提供了初始化列表的方式,来初始化类的属性,语法:构造函数():属性1(值1), 属性2(值2),...{}
一个示例:
#include <iostream>
using namespace std;
class Person {
public:
int m_A;
int m_B;
int m_C;
//Person(int a, int b, int c)
//{
// m_A = a;
// m_B = b;
// m_C = c;
// cout << "Person 的有参构造函数" << endl;
//}
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) //构造参数列表
{
};
~Person()
{
cout << "Person 的析构函数" << endl;
}
};
int main(void)
{
Person p1(10, 20, 30);
cout << p1.m_A << " " << p1.m_B << " " << p1.m_C << endl;
return 0;
}
3、静态成员变量
类对象作为类成员:C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
结论:当其他类对象作为本类的成员,构造时先构造其他类的对象,再构造自身的对象;析构时先析构自身类的对象,再析构其他类的对象。一个示例:
#include <iostream>
#include <string>
using namespace std;
class Phone {
public:
string m_PName;
Phone(string pname)
{
m_PName = pname;
cout << "Phone 的有参构造函数" << endl;
}
~Phone()
{
cout << "Phone 的析构函数" << endl;
}
};
class Person {
public:
string m_Name;
Phone m_Phone;
// 隐式转化法等价于 Phone m_Phone = pname
Person(string name, string pname) :m_Name(name), m_Phone(pname)
{
cout << "Person 的构造参数列表" << endl;
}
~Person()
{
cout << "Person 的析构函数" << endl;
}
};
void test01()
{
Person p("Nick", "Apple");
cout << p.m_Name << " holds on " << p.m_Phone.m_PName << endl;
}
int main(void)
{
test01();
return 0;
}
静态成员:静态成员就是在成员变量和成员函数前加上关键字static
,称为静态成员,静态成员分为:
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
**静态成员变量不属于具体的某个对象**,所有的对象都共享同一份数据,因此静态成员变量具有两种访问方式:
1、通过对象进行访问
2、通过类名进行访问,语法:
类名::属性名
静态成员变量也具有访问权限限制。一个示例:
#include <iostream>
using namespace std;
class Person {
public:
static int m_A; //类内声明
private:
static int m_B; //静态成员变量也有访问权限
};
int Person::m_A = 100; //类外初始化
int Person::m_B = 200;
void test(void)
{
Person p1;
cout << p1.m_A << endl;
Person p2;
p2.m_A = 200;
cout << p1.m_A << endl; //所有对象共享同一份数据
cout << Person::m_A << endl; //直接通过类名访问
//cout << p1.m_B << endl; //没有访问权限
}
int main(void)
{
test();
return 0;
}
静态成员函数只能访问静态成员变量,静态成员函数的访问方式与静态成员变量类似,也是有两种访问方式:
1、通过对象访问
2、通过类名访问,语法:
类名::函数名()
静态成员函数也是具有访问权限的,一个示例:
#include <iostream>
using namespace std;
class Person
{
public:
static int m_A;
int m_B;
static void func()
{
cout << m_A << endl;
m_A = 200;
cout << "静态成员函数" << endl;
cout << m_A << endl;
}
};
int Person::m_A = 100;
void test01()
{
Person p;
p.func(); //通过对象访问
Person::func(); //通过类名访问
}
int main(void)
{
test01();
return 0;
}
(3)Cpp对象模型和this
指针
1、this
指针
成员变量和成员函数是分开存储的:在Cpp中,类的成员变量和成员函数是分开存储的,只有非静态的成员变量才属于类的对象上,其余都不存储在类的对象上。空的类对象占用的空间为1,Cpp编译器会给每个空的类对象分配一个字节的空间,是为了区分空对象占内存的位置。
this
指针:每一个非静态成员函数都会产生一份函数的实例,也就是说多个同类型的对象会共用一块代码块,那么这共用的代码块怎么区分是那个对象调用自己呢?Cpp会通过提供特殊的对象指针this
,解决上述问题。(和python的self
类似)
this
指针指向被调用的成员函数所属的对象,this
指针是隐含每一个非静态成员函数的一种指针,this
指针不需要定义,直接使用即可。this
指针的用途如下:
- 当形参和成员变量同名时,可用
this
指针来进行区分;- 在类的非静态成员函数中返回对象本身,可用
return *this
示例:
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
//(* this).m_Age = age;
this->m_Age = age; //this指针指向类的对象,解决名称冲突的问题
}
Person& PersonAdd(Person& p)
{
this->m_Age += p.m_Age;
return *this; //返回this指针指向类对象
}
int m_Age;
};
void test01()
{
Person p1(18);
cout << p1.m_Age << endl;
}
void test02()
{
Person p1(10);
Person p2(18);
p2.PersonAdd(p1).PersonAdd(p1);
cout << p2.m_Age << endl;
}
int main(void)
{
test01();
test02();
return 0;
}
空指针访问成员函数:在Cpp中空指针也是可以访问成员函数的,但是要注意是否使用到了this
指针,如果使用了this
指针,需要保证代码的健壮性。示例:
#include <iostream>
using namespace std;
class Person
{
public:
int m_Age;
void ShowClassName()
{
cout << "Class name is Person" << endl;
}
void ShowAge()
{
if (this != NULL) //确保空指针不会解引用
cout << "Age is " << this->m_Age << endl;
else
return;
}
};
void test01()
{
Person* ptr = NULL;
ptr->ShowClassName(); //空指针正常访问
ptr->ShowAge();
}
int main(void)
{
test01();
return 0;
}
#### 2、常函数和常对象
const
修饰成员函数:
常函数:
- 成员函数声明后加
const
,称这个函数为常函数;- 常函数不可以修改成员属性;
- 成员属性声明时加关键字==
mutable
==后,在常函数中仍然可以修改。mutable
意为可变的。
常对象:
- 声明对象前加
cosnt
,称这个对象为常对象;- 常对象不允许修改(除了
mutable
修饰的)成员变量;- 常对象只能调用常函数。
关于常函数,这就涉及到this
指针的本质了,this
指针的本质是指针常量,指针的指向是不可修改的,但是指针的内容是可以修改的,类名 *const this
。**但我们在函数声明后面加上const
后,成为一个常函数,**本质是再次对this
指针进行了修饰,使this
指针成为==const 类名 *const this
==。这就限制了this
指针所指向的内容是不可以修改的。示例:
class Person
{
public:
int m_A;
mutable int m_B; //mutable是特殊声明,即使在常函数中,也是可以更改的
//this指针的本质是指针常量,指针的指向是不可修改的,但是指针的内容可以修改 Person* const this
//使用常函数const修饰之后,this指针变为 const Person* const this ,限制了其指向的内容也是不可以修改的
void ShowPerson() const //常函数
{
//m_A = 100;
m_B = 200; //常函数中使用成员变量
}
};
关于常对象,常对象也是不允许修改成员变量的,因此常对象不允许调用普通成员函数,因为普通成员函数可以修改成员变量。示例:
#include <iostream>
using namespace std;
class Person
{
public:
int m_A;
mutable int m_B; //mutable是特殊声明,即使在常函数中,也是可以更改的
//this指针的本质是指针常量,指针的指向是不可修改的,但是指针的内容可以修改 Person* const this
//使用常函数const修饰之后,this指针变为 const Person* const this ,限制了其指向的内容也是不可以修改的
void ShowPerson() const //常函数
{
//m_A = 100;
m_B = 200; //常函数中使用成员变量
}
void func()
{
cout << "Hello World" << endl;
m_A = 100;
}
};
void test01()
{
const Person p; //定义一个常对象
//p.func(); //常对象只能够使用常函数
}
int main(void)
{
test01();
return 0;
}
(4)友元
在程序中,有些私有属性也想让类外特殊的一些函数或者类进行访问,这就需要用到友元技术。友元的目的就是让一个函数或类访问另一个类中的私有成员,友元的关键字称为friend
。友元有三种实现:
- 全局函数做友元,语法:在类中加上
friend 全局函数的声明
;- 类做友元,语法:在类中加上
friend 类的声明
;- 其他类的成员函数做友元,语法:在类中加上
friend 其他的类名::成员函数声明
。
全局函数做友元的一个示例:
//全局函数做友元
#include <iostream>
#include <string>
using namespace std;
class Building
{
friend void goodfriend(Building& b); //告诉编译器goodfriend是Building类的友元
public:
string m_SittingRoom;
private:
string m_BedRoom;
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
};
void goodfriend(Building& b)
{
cout << "全局函数 正在访问:" << b.m_SittingRoom << endl;
cout << "全局函数 正在访问:" << b.m_BedRoom << endl; //访问私有变量
}
void test01()
{
Building build;
goodfriend(build);
}
int main(void)
{
test01();
return 0;
}
类做友元的一个示例:
// 类做友元
#include <iostream>
#include <string>
using namespace std;
class Building
{
friend class GoodFriend; //GoodFriend类是Building类的友元
public:
string m_SittingRoom;
private:
string m_BedRoom;
public:
Building(); //这里采用类外完成函数实现的方法
};
Building::Building()
{
m_BedRoom = "卧室";
m_SittingRoom = "客厅";
}
class GoodFriend
{
public:
Building* m_build;
GoodFriend();
~GoodFriend();
void visit();
};
GoodFriend::GoodFriend()
{
m_build = new Building;
}
GoodFriend::~GoodFriend()
{
if (m_build != NULL)
{
delete m_build;
m_build = NULL;
}
}
void GoodFriend::visit()
{
cout << "类 正在访问:" << m_build->m_SittingRoom << endl;
cout << "类 正在访问:" << m_build->m_BedRoom << endl;
}
void test01()
{
GoodFriend gf;
gf.visit();
}
int main(void)
{
test01();
return 0;
}
其他类的成员函数做友元的一个示例:
// 其他类的成员函数做友元
#include <iostream>
#include <string>
using namespace std;
class Building;
class GoodFriend
{
public:
Building* m_build;
GoodFriend();
~GoodFriend();
void visit1(); //做友元的成员函数,可以访问Building中的私有
void visit2(); //不做友元的成员函数,不可以访问Building中的私有
};
class Building
{
friend void GoodFriend::visit1(); //GoodFriend类中的visit1方法做为Building类的友元,可以访问其私有
public:
string m_SittingRoom;
private:
string m_BedRoom;
public:
Building();
};
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodFriend::GoodFriend()
{
m_build = new Building;
}
GoodFriend::~GoodFriend()
{
if (m_build != NULL)
{
delete m_build;
m_build = NULL;
}
}
void GoodFriend::visit1()
{
cout << "类的成员函数visit1 正在访问:" << m_build->m_SittingRoom << endl;
cout << "类的成员函数visit1 正在访问:" << m_build->m_BedRoom << endl;
}
void GoodFriend::visit2()
{
cout << "类的成员函数visit2 正在访问:" << m_build->m_SittingRoom << endl;
}
void test01()
{
GoodFriend gf;
gf.visit1();
gf.visit2();
}
int main(void)
{
test01();
return 0;
}
(5)运算符重载
运算符重载是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。在Cpp中重载的运算符是带有特殊名称的函数,函数名是由关键字operator
和其后面需要重载的运算符号构成的,例如重载+
则需要写成operator+
,运算符重载有两种方式:
- 通过成员函数进行重载;
- 通过全局函数进行重载;
- 运算符重载也可以进行函数重载。
例如重载加法运算符+
:
// 运算符重载
#include <iostream>
using namespace std;
class Person
{
public:
int m_A;
int m_B;
Person()
{}
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//1、成员函数方式运算符重载
Person operator+(Person& p)
{
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
Person operator+(int num) //3、运算符重载支持函数重载
{
this->m_A += num;
this->m_B += num;
return *this;
}
};
//2、全局函数方式运算符重载
//Person operator+(Person& a, Person& b)
//{
// Person temp;
// temp.m_A = a.m_A + b.m_A;
// temp.m_B = a.m_B + b.m_B;
// return temp;
//}
// 3、运算符重载支持函数重载
//Person operator+(Person& p, int num)
//{
// Person temp;
// temp.m_A = p.m_A + num;
// temp.m_B = p.m_B + num;
// return temp;
//}
void test01()
{
Person p1(10, 10);
Person p2(10, 10);
//1、成员函数重载调用本质
//Person p3 = p1.operator+(p2);
Person p3 = p1 + p2;
//2、全局函数重载调用本质
//Person p4 = operator+(p1, p2);
Person p4 = p1 + p2;
//3、运算符重载支持函数重载
//Person p5 = p1.operator+(5);
Person p5 = p1 + 5;
cout << "p3.m_A = " << p3.m_A << " p3.m_B = " << p3.m_B << endl;
cout << "p4.m_A = " << p4.m_A << " p4.m_B = " << p4.m_B << endl;
cout << "p5.m_A = " << p5.m_A << " p5.m_B = " << p5.m_B << endl;
}
int main(void)
{
test01();
return 0;
}
**重载左移运算符<<
**可以实现自定义的输出方式,示例:
// <<左移运算符重载
#include <iostream>
using namespace std;
class Person
{
friend ostream& operator<< (ostream& cout, Person& p); //利用友元
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//利用成员函数重载<< p.operator(cout) 简化版本就是 p << cout
//永远无法实现cout在左侧的运算,所有应该使用全局函数重载运算符的方法
//ostream& operator<<(ostream& cout)
//{
//}
private:
int m_A;
int m_B;
};
ostream& operator<<(ostream& cout, Person& p)
{
cout << "p.m_A = " << p.m_A << " p.m_B = " << p.m_B;
return cout;
}
void test01()
{
Person p(10, 20);
cout << p << " Hello World!" << endl;
}
int main(void)
{
test01();
return 0;
}
递增运算符++
重载,示例:
// 重载递增运算符++
#include <iostream>
using namespace std;
class MyInteger
{
friend ostream& operator<<(ostream& out, MyInteger myint); //友元
private:
int m_Num;
public:
MyInteger()
{
this->m_Num = 0; //默认初始化为0
}
//1、重载前置++
MyInteger& operator++()
{
this->m_Num++;
return *this;
}
//2、重载后置++
//void operator(int), int是一个占位参数,可以用于区分前置和后置递增,并且只能用int
MyInteger operator++(int)
{
MyInteger temp = *this; //先记录
this->m_Num++; //再递增
return temp;
}
};
ostream& operator<<(ostream& out, MyInteger myint) //重载<<运算符
{
out << myint.m_Num;
return out;
}
void test01()
{
MyInteger myint;
cout << myint << endl;
cout << ++myint << endl;
cout << myint++ << endl;
cout << myint << endl;
}
int main(void)
{
test01();
return 0;
}
总结:前置递增返回引用,后置递增返回值。
赋值运算符=
重载,Cpp编译器至少给一个类添加4个函数:
- 默认构造函数(无参,函数体为空);
- 默认析构函数(无参,函数体为空);
- 默认拷贝函数构造,对属性进行拷贝;
- 赋值运算符
operator=
,对属性进行值拷贝。
如果类中有属性指向堆区,那么做赋值操作的时候也会出现深浅拷贝的问题。**为什么需要赋值运算符重载?**这是因为编译器提供的赋值运算符具有深浅拷贝的问题。一个示例:
// 重载赋值操作符=
#include <iostream>
using namespace std;
class Person
{
public:
int* m_Age = NULL;
Person(int age)
{
this->m_Age = new int(age);
}
~Person() //存在深浅拷贝的问题
{
if (this->m_Age != NULL)
{
delete this->m_Age;
this->m_Age = NULL;
}
}
Person& operator=(Person& p) //重载赋值运算符
{
if (this->m_Age != NULL) //先判断是否已经在堆区分配了空间
{
delete this->m_Age;
m_Age = NULL;
}
this->m_Age = new int(*p.m_Age); //深拷贝
return *this; //返回对象本身,不能使用Person作为返回类型,因为这样会利用拷贝构造函数创建一个副本,并不会返回本身
}
};
void test01()
{
Person p1(20);
Person p2(0);
p2 = p1;
Person p3(0);
p3 = p2 = p1;
cout << "p1的年龄是:" << *p1.m_Age << endl;
cout << "p2的年龄是:" << *p2.m_Age << endl;
cout << "p3的年龄是:" << *p3.m_Age << endl;
}
int main(void)
{
test01();
return 0;
}
关系运算符重载,示例:
// 重载关系运算符
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string m_Name;
int m_Age;
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
}
//重载==
bool operator==(Person& p)
{
if (this->m_Age == p.m_Age and this->m_Name == p.m_Name)
return true;
else return false;
}
//重载>
bool operator>(Person& p)
{
if (this->m_Age > p.m_Age)
return true;
else return false;
}
};
void test01()
{
Person p1("Tom", 22);
Person p2("Tom", 22);
Person p3("Shane", 21);
if (p1 == p2)
cout << "p1和p2相等" << endl;
if (p2 > p3)
cout << "p2大于p3" << endl;
}
int main(void)
{
test01();
return 0;
}
函数调用运算符重载()
:
- 函数调用运算符
()
也可以重载;- 由于重载后使用的方式与函数的调用非常类似,因此称为仿函数;
- 仿函数没有固定的写法,非常灵活。
示例:
// 函数调用运算符重载()
#include <iostream>
#include <string>
using namespace std;
class MyPrint
{
public:
void operator()(string str)
{
cout << str << endl;
}
};
void MyPrint2(string str)
{
cout << str << endl;
}
void test01()
{
MyPrint mp;
//1、原始的调用应该是 mp.operator()("Hello World!")
mp.operator()("Hello World!");
//2、简化的调用
mp("Hello World!");
//3、函数的调用
MyPrint2("hello world!");
}
int main(void)
{
test01();
return 0;
}
另一个示例关于匿名函数对象:
//函数调用重载()
#include <iostream>
using namespace std;
class MyAdd
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
void test01()
{
MyAdd md; //创建一个函数对象
cout << md(1, 2) << endl;
//创建匿名函数对象,该行执行完毕立即被释放
cout << MyAdd()(2, 3) << endl;
}
int main(void)
{
test01();
return 0;
}
(6)继承
1、继承的基本方式
继承是面向对象的三大特性之一,继承的唯一好处是大大减少代码的冗余量,一个继承的简单示例:
// 继承
#include <iostream>
using namespace std;
class BasePage //父类
{
public:
void header()
{
cout << "首页、公开课、登陆...(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left()
{
cout << "Java、Python、CPP...(公共分类列表)" << endl;
}
};
class Java : public BasePage //Java类继承于BasePage父类
{
public:
void content()
{
cout << "Java学科视频" << endl;
}
};
class Python : public BasePage //Python类继承于BasePage父类
{
public:
void content()
{
cout << "Python学科视频" << endl;
}
};
class CPP : public BasePage //CPP类继承于BasePage父类
{
public:
void content()
{
cout << "CPP学科视频" << endl;
}
};
void test01()
{
Java j;
j.header();
j.footer();
j.left();
j.content();
cout << "----------------" << endl;
Python p;
p.header();
p.footer();
p.left();
p.content();
cout << "----------------" << endl;
CPP c;
c.header();
c.footer();
c.left();
c.content();
}
int main(void)
{
test01();
return 0;
}
总结一下,继承的语法:class 子类 : 继承方式 父类
。继承方式一共有三种:
- 公共继承
public
,子类继承父类的属性和方法的控制权限不变;- 保护继承
protected
,子类继承父类的属性和方法的控制权限变为protected
;- 私有继承
private
,子类继承父类的属性和方法的控制权限变为private
。
继承中的对象模型, 父类中所有非静态成员属性都会被子类继承下去,父类中私有成员属性是被编译器给隐藏了,因此是访问不到的,但是子类中仍然继承了父类中的私有成员属性。
继承中的构造和析构顺序,子类继承父类后,当创建子类对象也会调用父类的构造函数。具体的顺序如下:构造的顺序是先调用父类的构造函数,再调用子类的构造函数,析构的顺序是先调用子类的析构函数,再调用父类的析构函数。示例:
// 继承中的构造和析构顺序
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "父类的构造函数" << endl;
}
~Base()
{
cout << "父类的析构函数" << endl;
}
};
class Son : public Base
{
public:
Son()
{
cout << "子类的构造函数" << endl;
}
~Son()
{
cout << "子类的析构函数" << endl;
}
};
void test01()
{
Son s;
}
int main(void)
{
test01();
return 0;
}
继承中同名成员的处理方式:当父类与子类出现同名的成员,该如何通过子类对象,访问到子类或父类中的同名数据呢?
- 子类的同名成员变量和同名成员函数会隐藏掉父类的同名成员变量和同名成员函数;
- 通过子类对象访问子类同名数据,直接访问即可;
- 通过子类对象访问父类同名数据,需要加上作用域。
一个示例:
// 继承中同名成员变量的访问
#include <iostream>
using namespace std;
class Base
{
public:
int m_A;
Base()
{
m_A = 100;
}
void func()
{
cout << "Base的成员函数func调用" << endl;
}
};
class Son : public Base
{
public:
int m_A;
Son()
{
m_A = 200;
}
void func()
{
cout << "Son的成员函数func调用" << endl;
}
};
void test01()
{
Son s;
cout << "通过s访问Son中m_A = " << s.m_A << endl;
cout << "通过s访问Base中m_A = " << s.Base::m_A << endl; //访问父类的同名成员变量,加上父类的作用域即可
s.func();
s.Base::func(); //调用父类的成员函数
}
int main(void)
{
test01();
return 0;
}
继承同名静态成员的处理方式:静态成员和非静态成员出现同名的处理方式一致,如上三条要点。示例:
//继承中同名静态函数的处理方式
#include <iostream>
using namespace std;
class Base
{
public:
static int m_A;
static void func()
{
cout << "Base的静态成员函数" << endl;
}
};
int Base::m_A = 100;
class Son : public Base
{
public:
static int m_A;
static void func()
{
cout << "Son的静态成员函数" << endl;
}
};
int Son::m_A = 200;
void test01()
{
//1、通过对象来访问静态成员
Son s;
cout << "子类的静态成员变量m_A = " << s.m_A << endl;
cout << "父类的静态成员变量m_A = " << s.Base::m_A << endl;
s.func();
s.Base::func(); //父类的静态成员函数调用
//2、通过类名来访问静态成员
cout << "子类的静态成员变量m_A = " << Son::m_A << endl;
cout << "父类的静态成员变量m_A = " << Son::Base::m_A << endl;
Son::func();
Son::Base::func();
}
int main(void)
{
test01();
return 0;
}
2、多继承
多继承语法,在Cpp中允许一个类继承多个父类,具体语法:class 子类 : 继承方式 父类1, 继承方式 父类2...
,需要注意多继承可能引发父类中有同名成员出现的情况,此时需要加作用域进行区分。Cpp在实际开发中不建议使用多继承。示例:
//多继承语法
#include <iostream>
using namespace std;
class Base1
{
public:
int m_A;
Base1()
{
m_A = 100;
}
};
class Base2
{
public:
int m_A;
int m_B;
Base2()
{
m_A = 200;
m_B = 200;
}
};
class Son :public Base1, public Base2 //子类继承父类1和父类2
{
public:
int m_C;
Son()
{
m_C = 300;
}
};
void test01()
{
Son s;
cout << "子类继承父类的属性Base2 m_B = " << s.m_B << endl;
cout << "子类访问父类的同名属性Base1 m_A = " << s.Base1::m_A << endl; //同名成员属性仍需加作用域进行区分
cout << "子类访问父类的同名属性Base2 m_A = " << s.Base2::m_A << endl;
}
int main(void)
{
test01();
return 0;
}
菱形继承,菱形继承的概念是,有两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承方式就被称为菱形继承或钻石继承。
菱形继承会出现的问题,是数据的冗余和二义性,由于最底层的派生类继承了两个基类,同时这两个基类又继承的是同一个父类,故而会造成最底部基类的两次调用,造成了数据的冗余和二义性问题。解决办法是采用虚继承的方式。问题演示:
//菱形继承出现的问题
#include <iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
class Sheep : public Animal{};
class Camel : public Animal{};
class YangTuo : public Sheep, public Camel{};
void test01()
{
YangTuo t;
t.Sheep::m_Age = 10;
t.Camel::m_Age = 20;
cout << "t.Sheep::m_Age = " << t.Sheep::m_Age << endl; //出现了成员属性的冗余
cout << "t.Camel::m_Age = " << t.Camel::m_Age << endl; //只能通过作用域来区分
}
int main(void)
{
test01();
return 0;
}
虚继承,利用关键字==virtual
,在继承之前加上关键字virtual
变为虚继承,语法:class 子类 : virtual 继承方式 父类
==。虚继承是一种机制,类通过虚继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。示例:
//使用虚继承
#include <iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
class Sheep : virtual public Animal{};
class Camel : virtual public Animal{};
class YangTuo : public Sheep, public Camel{};
void test01()
{
YangTuo t;
t.Sheep::m_Age = 10;
t.Camel::m_Age = 20;
cout << "t.Sheep::m_Age = " << t.Sheep::m_Age << endl; //只有一份m_Age成员属性了,可以通过三种方式访问
cout << "t.Camel::m_Age = " << t.Camel::m_Age << endl;
cout << "t.m_Age = " << t.m_Age << endl;
}
int main(void)
{
test01();
return 0;
}
(7)多态
1、多态概念
多态是Cpp面向对象的三大特性之一,多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名;
- 动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态的区别在于:
- 静态多态的函数地址早绑定,编译阶段确定函数地址;
- 动态多态的函数地址晚绑定,运行阶段确定函数地址。
动态多态的满足条件:
- 有继承关系;
- 子类要重写父类的虚函数,虚函数就是在函数的声明前加上==
virtual
==关键字,重写是指函数返回值的类型和函数的参数列表完全一致,子类重写父类虚函数的关键字virtual
可写可不写;- 父类的指针或引用指向子类的对象,并且执行子类对象重写父类的虚函数。
父类的指针可以直接指向子类对象,示例:
//动态多态
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void Speek() //2、子类重写父类的虚函数
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal //1、有继承关系
{
public:
virtual void Speek() //2、子类重写父类的虚函数
{
cout << "猫咪在说话" << endl;
}
};
void dospeek(Animal& animal) //3、父类的指针或引用指向子类对象
{
animal.Speek(); //3、并且调用子类重写的虚函数
}
void test01()
{
Cat cat;
dospeek(cat);
}
int main(void)
{
test01();
return 0;
}
动态多态的原理解析,父类的虚函数会产生一个虚函数指针(vfptr)和虚函数表(vftable),该虚函数指针指向虚函数表;当子类继承父类之后,继承了相应的虚函数指针(vfptr)和虚函数表(vftable),当子类重写了父类的虚函数的时候,相当于在子类继承的虚函数表中,进行了更改,覆盖了原来父类的虚函数,而是改为了子类重写的虚函数;因此,当父类的指针(引用)指向子类的对象,并且调用虚函数的时候,此时调用的是子类重写的虚函数,这就是动态多态的原理。
为什么要使用多态?多态的主要目的是通过一个父类的指针,调用多个子类的虚函数。多态的优点在于:
- 代码组织结构清晰;
- 可读性强;
- 利于前期和后期的扩展和维护。
记住开闭原则,对修改进行关闭,对扩展进行开放。
多态案例——计算器类,示例:
//计算器的传统写法
#include <iostream>
#include <string>
using namespace std;
class Calculator //传统的写法
{
public:
int m_Num1;
int m_Num2;
int getResult(string oper)
{
if (oper == "+")
return m_Num1 + m_Num2;
else if (oper == "-")
return m_Num1 - m_Num2;
else if (oper == "*")
return m_Num1 * m_Num2;
}
};
void test01()
{
Calculator cal;
cal.m_Num1 = 10;
cal.m_Num2 = 10;
cout << cal.m_Num1 << " + " << cal.m_Num2 << " = " << cal.getResult("+") << endl;
cout << cal.m_Num1 << " - " << cal.m_Num2 << " = " << cal.getResult("-") << endl;
cout << cal.m_Num1 << " * " << cal.m_Num2 << " = " << cal.getResult("*") << endl;
}
int main(void)
{
test01();
return 0;
}
// 计算器的多态写法
#include <iostream>
using namespace std;
class AbstractCalculator
{
public:
int m_Num1;
int m_Num2;
virtual int getResult()
{
return 0;
}
};
class AddCalculator : public AbstractCalculator
{
public:
virtual int getResult()
{
return m_Num1 + m_Num2;
}
};
class SubCalculator : public AbstractCalculator
{
public:
virtual int getResult()
{
return m_Num1 - m_Num2;
}
};
class MulCalculator : public AbstractCalculator
{
public:
virtual int getResult()
{
return m_Num1 * m_Num2;
}
};
void test01()
{
AbstractCalculator* ptr = new AddCalculator; //父类的指针指向子类的对象
ptr->m_Num1 = 10;
ptr->m_Num2 = 10;
cout << ptr->m_Num1 << " + " << ptr->m_Num2 << " = " << ptr->getResult() << endl;
delete ptr;
ptr = new SubCalculator;
ptr->m_Num1 = 10;
ptr->m_Num2 = 10;
cout << ptr->m_Num1 << " - " << ptr->m_Num2 << " = " << ptr->getResult() << endl;
}
int main(void)
{
test01();
return 0;
}
2、虚函数和抽象类
纯虚函数和抽象类,在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将父类中的虚函数改为纯虚函数,纯虚函数的语法:virtual 返回值类型 函数名 (参数列表) = 0;
,当类有了纯虚函数,这个类也称为==抽象类==。抽象类的特点是:
- 无法实例化对象;
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
一个示例:
//纯虚函数
#include <iostream>
using namespace std;
class Base //抽象类
{
public:
virtual void func() = 0; //纯虚函数
};
class Son : public Base
{
public:
virtual void func()
{
cout << "Son的重写虚函数" << endl;
}
};
void test01()
{
//Base s; //抽象类不能够创建对象
//Base* ptr = new Base; //抽象类也不能开辟空间
Base* ptr = new Son; //多态
ptr->func();
}
int main(void)
{
test01();
return 0;
}
多态案例二——制作饮品,示例:
// 案例二——制作饮品
#include <iostream>
using namespace std;
class AbstractDrinking //抽象类
{
public:
virtual void Boil() = 0; //煮水
virtual void Brew() = 0; //冲泡
virtual void Pour() = 0; //倒入容器
virtual void Put() = 0; //加料
void makedrink() //调用接口
{
Boil();
Brew();
Pour();
Put();
}
};
class Coffee :public AbstractDrinking //咖啡类
{
public:
virtual void Boil()
{
cout << "煮矿泉水" << endl;
}
virtual void Brew()
{
cout << "冲泡咖啡" << endl;
}
virtual void Pour()
{
cout << "倒入咖啡杯" << endl;
}
virtual void Put()
{
cout << "加入牛奶" << endl;
}
};
class Tea :public AbstractDrinking
{
public:
virtual void Boil()
{
cout << "煮纯净水" << endl;
}
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
virtual void Pour()
{
cout << "倒入茶杯" << endl;
}
virtual void Put()
{
cout << "加入柠檬" << endl;
}
};
void doWork(AbstractDrinking* abs) //多态的用处,用一个抽象类指针,调用不同的接口
{
abs->makedrink();
delete abs;
}
void test01()
{
doWork(new Coffee);
cout << "---------" << endl;
doWork(new Tea);
}
int main(void)
{
test01();
return 0;
}
3、虚析构
虚析构和纯虚析构,为什么需要虚析构和纯虚析构?在多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,这就会导致内存泄漏。
解决方法就是将父类中的析构函数改为虚析构或纯虚析构。虚析构和纯虚析构的异同:
相同点:
- 可以解决父类指针释放子类对象的问题;
- 都需要有具体的函数实现。
不同点:
- 虚析构,该类不属于抽象类,可以实例化对象;
- 如果是纯虚析构,该类属于抽象类,无法实例化对象。
虚析构和纯虚析构析构的语法:
- 虚析构的语法:
virtual ~类名(){}
- 纯虚析构的语法:
virtual ~类名()=0;
- 纯虚析构虽然叫纯虚析构,但是仍然要在类外完成析构函数的函数体,语法:
类名::~类名(){}
利用虚析构或纯虚析构可以解决父类指针释放子类对象的问题,示例:
//虚析构和纯虚析构
#include <iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal的构造函数调用" << endl;
}
//~Animal() //这样Animal*类型的指针无法调用Cat类的析构函数,造成内存泄漏,解决方法是采用虚析构函数
//{
// cout << "Animal的析构函数调用" << endl;
//}
//利用虚析构可以解决父类指针释放子类对象时内存泄漏的问题
virtual ~Animal() //虚析构
{
cout << "Animal的析构函数调用" << endl;
}
virtual void speek() = 0; //纯虚函数
};
class Cat : public Animal
{
public:
string* m_Name;
Cat(string name)
{
cout << "Cat的构造函数调用" << endl;
m_Name = new string(name);
}
~Cat()
{
cout << "Cat的析构函数调用" << endl;
if (m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
virtual void speek()
{
cout << *m_Name << "小猫在说话" << endl;
}
};
void test01()
{
Animal* an = new Cat("Tom");
an->speek();
delete an;
}
int main(void)
{
test01();
return 0;
}
使用纯虚析构,将上述代码中的Animal类改为:
class Animal //该类属于抽象类,无法实例化对象
{
public:
Animal()
{
cout << "Animal的构造函数调用" << endl;
}
virtual ~Animal() = 0; //纯虚析构
virtual void speek() = 0; //纯虚函数
};
Animal::~Animal() //纯虚析构函数的函数体
{
cout << "Animal的析构函数调用" << endl;
}
小结:
- 虚析构和纯虚析构就是用来解决父类指针释放子类对象的问题;
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构;
- 拥有纯虚析构函数的类也属于抽象类。
多态案例三——电脑组装
//案例三电脑组装——多态的好处
#include <iostream>
using namespace std;
class CPU //抽象类
{
public:
virtual void calculate() = 0;
};
class GPU //抽象类
{
public:
virtual void display() = 0;
};
class Memory //抽象类
{
public:
virtual void storage() = 0;
};
class Computer //组装电脑类
{
public:
CPU* cpu; //抽象的父类指针指向子类对象
GPU* gpu;
Memory* mem;
Computer(CPU* c, GPU* g, Memory* m)
{
cpu = c;
gpu = g;
mem = m;
}
~Computer() //析构函数
{
if (cpu != NULL)
{
delete cpu;
cpu = NULL;
}
if (gpu != NULL)
{
delete gpu;
gpu = NULL;
}
if (mem != NULL)
{
delete mem;
mem = NULL;
}
}
void makeup() //组装电脑
{
cpu->calculate();
gpu->display();
mem->storage();
cout << "-------------------" << endl;
}
};
//Intel厂商
class IntelCpu :public CPU
{
public:
virtual void calculate()
{
cout << "Intel的CPU正在计算" << endl;
}
};
class IntelGpu :public GPU
{
public:
virtual void display()
{
cout << "Intel的GPU正在显示" << endl;
}
};
class IntelMem : public Memory
{
public:
virtual void storage()
{
cout << "Intel的内存条正在储存" << endl;
}
};
//AMD厂商
class AMDCpu :public CPU
{
public:
virtual void calculate()
{
cout << "AMD的CPU正在计算" << endl;
}
};
class AMDGpu :public GPU
{
public:
virtual void display()
{
cout << "AMD的GPU正在显示" << endl;
}
};
class AMDMem :public Memory
{
public:
virtual void storage()
{
cout << "AMD的内存条正在存储" << endl;
}
};
void test01()
{
IntelCpu* icptr = new IntelCpu;
IntelGpu* igptr = new IntelGpu;
IntelMem* imptr = new IntelMem;
AMDCpu* acptr = new AMDCpu;
AMDGpu* agptr = new AMDGpu;
AMDMem* amptr = new AMDMem;
Computer* intel = new Computer(icptr, igptr, imptr);
intel->makeup();
Computer* amd = new Computer(acptr, agptr, amptr);
amd->makeup();
delete intel;
delete amd;
}
int main(void)
{
test01();
return 0;
}