目录
对象与类
类的语法:
C++中class与struct的区别:
通过类实例化对象的方式
具体案例
类作用域与分文件编写
创建circle.h头文件
创建源文件circle.cpp
创建all.cpp来作为程序的入口
封装
封装的意义
访问权限符
成员属性私有化
优点
具体案例
对象的初始化和清理
构造函数
析构函数
具体案例
构造函数的分类及调用
构造函数的分类
具体案例
构造函数的调用
括号法
显示法
隐式转换法
匿名对象
拷贝构造函数的调用时机
构造函数的调用规则
默认情况下C++编译器至少给一个类添加3个函数
构造函数的调用规则
初始化列表
深拷贝与浅拷贝
类对象作为类成员
静态成员
前言:
静态成员的访问方式
静态成员变量
静态成员函数
成员变量和成员函数分开存储
this指针
定义
this指针的用途
空指针访问成员函数
const修饰成员
常函数:
常对象
友元
前言:
友元的三种实现
全局函数做友元
类做友元
成员函数做友元
运算符重载
重载运算符方式
加号运算符重载
通过成员函数进行重载
通过全局函数进行重载
左移运算符重载
通过全局函数重载左移运算符
递增运算符重载
赋值运算符重载
C++编译器至少会给一个类添加4个函数
关系运算符重载
重载==号
函数调用运算符重载
前言:
具体案例
继承
类与类之间的继承关系
继承语法:
继承的经典案例
继承的方式种类
理解结构图
继承中的对象属性
继承中构造和析构的顺序
继承同名成员处理方式
继承中同名静态成员的处理方法
C++中的多继承
语法:
菱形继承
经典案例
菱形继承带来的问题
虚继承
前言:
多态
多态的分类
静态多态和动态多态的区别
动态多态的满足条件
案例分析
动态多态的原理剖析
纯虚函数和抽象类
抽象类特点
虚析构和纯虚析构
虚析构和纯虚析构共性与区别
语法:
经典案例
总结:
对象与类
- C++中认为万事万物皆对象,对象上有其属性和行为
- 属性可以理解为对象的成员变量
- 行为可以理解为对象所拥有的方法
- 类中的属性和行为统一称为成员
- 我们把具有相同性质的对象抽象为一个类
类的语法:
class 类名{
访问权限1:
属性1;
行为1;
访问权限2:
属性2;
行为2;
};
C++中class与struct的区别:
- struct默认权限为public
- class默认权限为private
注意:
- C++中的类用关键字class修饰
- 在C++中结构体内也可以使用权限修饰符,也可以有成员变量以及成员方法
通过类实例化对象的方式
语法:类名 对象名;
注意:实例化对象之后就可以通过对象名.成员名的方式来访问类中的属性及方法
具体案例
#include <iostream>
using namespace std;
const double PI = 3.14;
//创建圆类
class Circle {
//访问权限
public:
//属性
int r;
//行为
double calculateZC() {
return 2 * PI * r;
}
};
void main() {
//通过圆类来实例化圆的对象
Circle c;
c.r = 10;
cout << "圆的周长为:" << c.calculateZC() << endl;
system("pause");
}
类作用域与分文件编写
创建circle.h头文件
//防止头文件重复包含
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
const double PI = 3.14;
//创建圆类
class Circle {
public:
//设置半径
void setR(double newr);
//访问半径
double getR();
//求圆的周长
double calculateZC();
private:
//属性
int r;
};
注意:头文件中只写函数声明,不写函数实现。
创建源文件circle.cpp
#include "circle.h"
//设置半径(circle作用域下的成员函数)
void Circle::setR(double newr) {
r = newr;
}
//访问半径
double Circle::getR() {
return r;
}
//求圆的周长
double Circle::calculateZC() {
return 2 * PI * r;
}
注意:源文件中只写函数的实现,在使用时需要引入对应的头文件,并用成员作用域的方式(类名::函数名)明确要实现的是哪个类的哪个方法
创建all.cpp来作为程序的入口
#include "circle.h"
void main() {
//通过圆类来实例化圆的对象
Circle c;
//设置圆的半径
c.setR(10);
cout << "圆的半径为:" << c.getR() << endl;
cout << "圆的周长为:" << c.calculateZC() << endl;
system("pause");
}
注意:使用时需要导入circle.h头文件
C++面向对象的三大特性:封装、继承、多态。
封装
封装的意义
- 将属性和行为作为一个整体来表现生活中的事物
- 将属性和行为通过权限进行控制
访问权限符
- public:公共权限(成员类内可以访问该属性,类外同样可以访问该属性)
- protected:保护权限(该属性在类内可以访问,类外不可以访问)
- private:私有权限(该属性在类内可以访问,类外不可以访问)
protected与private的区别:后面我们会讲到继承,在继承中若父类的属性访问权限修饰符为protected,那么子类就可以访问该父类属性; 若父类的属性访问权限修饰符为private,那么子类就不可以访问该父类属性;
成员属性私有化
优点
- 将成员属性设置成私有化,可以自己控制读写权限
- 对于写的权限,我们可以检测数据的有效性(在方法内加条件)
具体案例
#include <iostream>
using namespace std;
//创建person类
class Person
{
public:
//设置姓名
void setName(string name) {
m_Name = name;
}
//读取姓名
string getName() {
return m_Name;
}
//读取年龄
int getAge() {
return m_Age;
}
//更改爱人
void setLover(string lover) {
m_Lover = lover;
}
private:
//姓名——可读可写
string m_Name="lili";
//年龄——只读
int m_Age=18;
//爱人——只写
string m_Lover="lan";
};
void main() {
Person people;
string my=people.getName();
cout << "我的名字为:" << my << endl;
people.setName("Dong");
my=people.getName();
cout << "我的名字为:" << my << endl;
system("pause");
}
注意:被private修饰的属性已经无权限访问,只能通过public修饰的方法对该属性进行间接访问
对象的初始化和清理
- 一个对象或者变量没有初始的状态,那么对其使用的后果是未知的
- 使用完一个对象或者变量,没有及时清理,那么也会造成一定的安全问题
前言:C++利用了构造函数和析构函数来解决以上问题,这两个函数将被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要求我们做的事情,因此,若我们不提供构造和析构那么编译器会提供。但是编译器提供的构造和析构函数都是空实现
构造函数
语法:类名(){}
构造函数作用:主要作用是创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
注意:
- 构造函数没有返回值,也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时自动调用构造,无需手动调用,而且只会调用一次
析构函数
语法:~类名(){}
注意:
- 析构函数没有返回值,也不写void
- 函数名称与类名相同,并且在名称前加~
- 析构函数不可以有参数,不可以发生重载
- 程序在自动销毁前会自动调用析构函数,无需手动调用,而且只会调用一次
- 若属性创建在堆区,那么析构函数在执行delete的语句后方可调用
析构函数作用:主要作用在于对象销毁前系统自动调用,执行一些清理工作
具体案例
#include <iostream>
using namespace std;
class Person {
public:
Person() {
cout << "Person构造函数的调用" << endl;
}
~Person() {
cout << "Person析构函数的调用" << endl;
}
};
void main() {
Person p;
system("pause");
}
构造函数的分类及调用
构造函数的分类
- 按照参数:有参构造和无参构造
- 按照类型:普通构造和拷贝构造
具体案例
class Person {
public:
Person() {
cout << "Person无参构造函数的调用" << endl;
}
Person(string name) {
p_Name = name;
cout << "Person的含参构造函数的调用" << endl;
}
Person(string name,int age,string sex) {
p_Name = name;
p_Age = age;
p_Sex = sex;
cout << "Person的全参构造函数的调用" << endl;
}
Person(const Person& p) {
p_Name = p.p_Name;
p_Age = p.p_Age;
p_Sex = p.p_Sex;
cout << "Person的拷贝构造函数的调用" << endl;
}
private:
string p_Name;
int p_Age;
string p_Sex;
};
注意:若不写构造函数那么编译器会自动调用自己的无参构造,若自己写了构造函数,那么编译器提供的构造函数就不可用。
构造函数的调用
- 括号法
- 显示法
- 隐式转换法
括号法
//括号法
void kuoHaoFa() {
Person p; //默认构造函数的调用
Person p1("lili"); //含参构造函数的调用
Person p2(p1); //拷贝构造函数的调用
}
注意:调用默认构造函数的时候不要加()——因为Person p();编译器会认为它是一个函数声明,不会认为是在创建对象
显示法
//显示法
void xianShiFa() {
//默认构造函数的调用
Person p;
//含参构造函数的调用
Person p1 = Person("lili");
}
注意:
- 该参数列表是为了调用含参构造所用的
- 不要利用拷贝函数来初始化一个匿名对象——Person(p)==Person p
隐式转换法
//隐式转换法
void YinShiFa() {
//默认构造函数的调用
Person p;
//含参构造函数的调用
string name = "lili";
//等同:Person p1=Person("lili");
Person p1 = name;
}
注意:隐式转换只有在构造函数有单个形参的情况下才可以进行
匿名对象
匿名对象:类名(参数列表);
匿名对象特点:当执行结束后,系统会立即回收掉匿名对象
拷贝构造函数的调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象
- 以值传递的方式给函数传参(实参传递给形参时会调用拷贝构造函数,拷贝一个新的对象传给形参)
- 以值的方式返回局部对象(会将返回的对象(局部对象)拷贝一份传给接受的变量)
#include <iostream>
using namespace std;
class Person {
public:
Person() {
cout << "Person无参构造函数的调用" << endl;
}
Person(const Person& p) {
p_Name = p.p_Name;
p_Age = p.p_Age;
p_Sex = p.p_Sex;
cout << "Person的拷贝构造函数的调用" << endl;
}
private:
string p_Name;
int p_Age;
string p_Sex;
};
//以值传递的方式给函数传参,会调用到拷贝构造函数
void doWork(Person p) {
cout << "dowork函数" << endl;
}
//以值的方式返回局部对象,会调用到拷贝构造函数
Person doWork1() {
cout << "dowork1函数" << endl;
Person p1;
return p1;
}
void main() {
Person p;
doWork(p);
Person p2=doWork1();
system("pause");
}
构造函数的调用规则
默认情况下C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认的拷贝构造函数,对属性进行拷贝
构造函数的调用规则
- 若用户定义了有参构造函数,那么C++不再提供默认的无参构造,但是会提供默认的拷贝构造
- 若用户定义了拷贝构造函数,那么C++将不再提供其他的构造函数
初始化列表
作用:C++提供了初始化列表语法:用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)……{}
#include <iostream>
using namespace std;
class Person {
public:
//初始化列表来初始化属性
Person(int a,int b,int c) :m_A(a), m_B(b), m_C(c) {
cout << "m_A=" <<a<< endl;
cout << "m_B=" <<b<< endl;
cout << "m_C=" <<c<< endl;
cout << "初始化列表函数" << endl;
}
int m_A;
int m_B;
int m_C;
};
void main() {
//测试初始化列表
Person p(10,20,30);
system("pause");
}
深拷贝与浅拷贝
浅拷贝:浅拷贝就是对象的数据之间的简单赋值;原始的数据就占用一份空间,而两个指针会共同指向原始数据所占的地址
深拷贝:深拷贝相对于浅拷贝他会在堆内存中另外申请空间来储存数据,两个指针分别会指向两块不同的内存空间
#include <iostream>
using namespace std;
class Person {
public:
Person(int age) {
m_Age = new int(age);
}
//浅拷贝带来问题,堆区内存重复释放
~Person() {
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
}
//重载赋值运算符
Person& operator=(Person& p) {
//先判断是否有属性在堆区,若有先释放干净再深拷贝
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
//深拷贝
m_Age =new int(*p.m_Age);
return *this;
}
int* m_Age;
};
void main() {
Person p1(18);
cout << "p1的年龄为:" << *(p1.m_Age) << endl;
Person p2(20);
//若不进行赋值运算符重载则为浅拷贝
p2 = p1;
cout << "p2的年龄为:" << *(p2.m_Age) << endl;
}
注意:
- C++中默认的拷贝构造函数就是浅拷贝
- 若属性是在堆区开辟的,那么一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
- 当拷贝一个对象的时候若需要拷贝这个对象所引用的对象,则是深拷贝,否则为浅拷贝
类对象作为类成员
前言:C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
class A{}
class B{
A a;
}
注意:B类中有对象A作为成员,A为对象成员
#include <iostream>
using namespace std;
class Phone {
public:
string p_Name;
Phone(string name) {
p_Name = name;
}
};
class Person {
public:
Person(string name, string pname):m_Name(name),m_Phone(pname) {
cout << "初始化列表" << endl;
}
string m_Name;
Phone m_Phone;
};
void main() {
Person p("lili", "apple");
cout << "name:" << p.m_Name << endl;
cout << "pname:" << p.m_Phone.p_Name << endl;
system("pause");
}
注意:当其他类对象作为本类的成员,构造时候先构造本类的对象,再构造自身;先析构自身再析构本类的对象
静态成员
前言:
- 静态成员就是在成员变量或成员函数上加上关键字static,称为静态成员
- 静态成员不属于某个对象,所有的对象都共享同一份数据
- 静态成员也是具有访问权限的,也遵循权限访问的规则
静态成员的访问方式
- 通过对象进行访问(对象.静态成员)
- 通过类名进行访问(类名::静态成员)
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
#include <iostream>
using namespace std;
class Person {
public:
//类内声明m_A
static int m_A;
};
//类外初始化
int Person::m_A = 23;
void main() {
Person p;
Person p1;
p1.m_A = 200;
//通过对象进行访问
cout << "m_A:" << p.m_A << endl;//200
//通过类名进行访问
cout << "m_A:" << Person::m_A << endl;//200
system("pause");
}
静态成员函数
- 所有的对象共享同一份函数
- 静态成员函数只能访问静态成员变量
#include <iostream>
using namespace std;
class Person {
public:
static void func() {
//若访问非静态成员变量后则会报错
m_A = 100;
cout << "静态成员方法func的调用,m_A=" <<m_A<< endl;
}
static int m_A;
};
int Person::m_A = 23;
void main() {
Person p;
//通过对象进行访问
p.func();
//通过类名进行访问
Person::func();
system("pause");
}
注意:静态成员函数不可以访问非静态的成员变量,主要原因是静态成员所有的对象共享一份,若访问对应的非静态成员变量则不清楚访问的是哪个对象的成员变量
成员变量和成员函数分开存储
class Person {
public:
int m_A; //非静态成员变量,属于类的对象上,占用类对象的空间
static int m_B; //静态成员变量,不在类的对象上,不占用类对象的空间
void func() {} //非静态成员函数,不在类的对象上,不占用类对象的空间
static void func1(){} //静态成员函数,不在类的对象上,不占用类对象的空间
};
注意:
- 空对象所占用的内存为1,C++编译器也会给每个空对象分配一个字节空间,是为了区分空对象所占内存的位置,每个空对象也应该有个独一无二的地址
- 成员变量和成员函数是分开存储的,成员函数以及静态成员不存在类的对象上,非静态成员变量存在于类的对象上
this指针
问题:C++中的成员变量与成员函数分开存储,每个非静态成员函数只会诞生出一份函数实例,那么该如何区分是哪个对象调用这个函数呢
定义
含义:this指针指向被调用的成员函数所属的对象
注意:
- this指针是隐含每一个非静态成员函数内的一种指针
- this指针不需要定义,直接使用即可
this指针的用途
- 当形参和成员变量同名时,可用this指针来进行区分
- 在类的非静态成员函数中返回对象本身,可使用return *this;
#include <iostream>
using namespace std;
class Person {
public:
//解决名称冲突
Person(int age) {
this->age = age;
}
int age;
Person& PersonAdd(Person& p) {
this->age += p.age;
//返回调用该函数的对象
return *this;
}
};
void main() {
Person p(18);
Person p1(12);
cout << "p的age:" << p.age << endl;
p1.PersonAdd(p).PersonAdd(p);
cout << "p1的age:" << p1.age << endl;
system("pause");
}
注意:若PersonAdd函数以Person为返回值返回,那么返回的就不是哪个特定地址的person对象了,而是经过拷贝构造函数拷贝的新对象,因此一定要以引用的方式(Person&)返回
空指针访问成员函数
前言:C++中空指针也可以调用成员函数的,但是需要注意有没有用到this指针
#include <iostream>
using namespace std;
class Person {
public:
void showClassName() {
cout << "this is Person class" << endl;
}
void showPersonAge() {
cout << "age=" << this->m_Age << endl;
}
int m_Age=18;
static string m_Name;
};
string Person::m_Name = "lili";
void main() {
Person *p=NULL;
//调用正常,因为没有访问具体对象内的资源
p->showClassName();
//调用正常,空指针可以调用被static修饰的静态常量
cout << "name=" << p->m_Name << endl;
//调用失败,因为空指针不能访问对象内的资源
cout << "age=" << p->m_Age << endl;
//调用失败,因为空指针访问了具体对象
p->showPersonAge();
system("pause");
}
注意:空指针也可以访问类资源,以及不存在于对象上的资源,但是不可以访问相关对象上的资源
const修饰成员
常函数:
- 成员函数后加const后我们就称该函数为常函数
- 常函数不可以修饰成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person {
public:
//常函数
void showPerson() const {
//常函数内m_A不可以修改
//this->m_A = 100;
//加了mutable关键字后即使在常函数中也可以修改该值
this->m_B = 300;
}
int m_A;
mutable int m_B;
};
注意:
- this指针的本质:指针常量,指针的指向不可以修改(就是指向本类对象)。
- 在成员函数后面加const本质上就是修饰该函数的this指针,进而使得this指针指向的值也不可以修改
常对象
- 声明对象前加const,那么就称该对象为常对象
- 常对象只能调用常函数,以及修改被mutable修饰的成员属性
#include <iostream>
using namespace std;
class Person {
public:
//常函数
void showPerson() const {
//常函数内m_A不可以修改
//this->m_A = 100;
//加了mutable关键字后即使在常函数中也可以修改该值
this->m_B = 300;
}
int m_A;
mutable int m_B;
};
void main() {
//常对象
const Person p;
p.m_B = 39;
cout << "m_B:" << p.m_B << endl;//39
//常对象只能调用常函数
p.showPerson();
cout << "m_B:" << p.m_B << endl;//300
system("pause");
}
友元
前言:
- 在程序里,有些私有属性也想让类外的一些特殊的函数或者类进行访问,那么就需要友元技术
- 友元的目的:让一个函数或者类访问另一个类的私有成员
- 友元的关键字为friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
#include <iostream>
using namespace std;
class Building {
//全局函数做友元(说明goodGay函数是该类的好朋友,可以访问该类的私有属性)
friend void goodGay(Building* building);
public:
Building() {
m_Room = "客厅";
m_BedRoom = "卧室";
}
//客厅
string m_Room;
private:
//卧室
string m_BedRoom;
};
//全局函数
void goodGay(Building *building) {
cout << "好基友全局函数,正在访问:" << building->m_BedRoom << endl;
}
void main() {
Building building;
goodGay(&building);
}
注意:友元的声明不需要放在权限修饰符内
类做友元
#include <iostream>
using namespace std;
class GoodGay {
public:
Building* building;
GoodGay() {
building = new Building;
}
void visit() {
cout << "好基友的类正在访问:" << building->m_BedRoom << endl;
};
};
class Building {
//类做友元,GoodGay类可以访问Building类的私有属性
friend class GoodGay;
public:
Building() {
m_Room = "客厅";
m_BedRoom = "卧室";
}
string m_Room;
private:
string m_BedRoom;
};
void main() {
GoodGay g;
g.visit();
}
成员函数做友元
#include <iostream>
using namespace std;
//告诉编译器有该类,解决visit访问不到类内属性的问题
class Building;
class GoodGay {
public:
//让visit函数可以访问到Building的私有成员
void visit();
//让visit1函数不可以访问到Building的私有成员
void visit1();
GoodGay();
private:
Building *building;
};
class Building {
//成员方法做友元
friend void GoodGay::visit();
public:
Building();
string m_Room;
private:
string m_BedRoom;
};
Building::Building() {
m_Room = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay() {
building = new Building;
}
void GoodGay::visit() {
cout << "好基友的类正在访问:" << building->m_BedRoom << endl;
}
void GoodGay::visit1() {
cout << "好基友的类正在访问building->m_BedRoom,但是访问不到!" << endl;
}
void main() {
GoodGay g;
g.visit();
g.visit1();
}
注意:声明成员函数为友元时必须确定该函数是存在的
运算符重载
概念:对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型
重载运算符方式
- 通过成员函数重载
- 通过全局函数重载
加号运算符重载
通过成员函数进行重载
语法:将方法名改为——operator运算符
作用:实现两个自定义数据类型相加的运算
说明:p=p1+p2 <=> p=p1.operator+(p2)
#include <iostream>
using namespace std;
class Person {
public:
int m_A;
int m_B;
Person operator+(Person& p) {
Person addP;
addP.m_A = this->m_A + p.m_A;
addP.m_B = this->m_B + p.m_B;
return addP;
}
};
void main() {
Person p;
p.m_A = 20;
p.m_B = 11;
Person p1;
p1.m_A = 30;
p1.m_B = 9;
Person p2 = p + p1;
cout << "p2的m_A:" << p2.m_A << "p2的m_B:" << p2.m_B << endl;//m_A:50 m_B:20
}
注意:这里的p+p1等价于p.operator+(p1)
通过全局函数进行重载
说明:p=p1+p2 <=> p=operator+(p1,p2)
#include <iostream>
using namespace std;
class Person {
public:
int m_A;
int m_B;
};
Person operator+(Person& p,Person& p1) {
Person addP;
addP.m_A = p.m_A + p1.m_A;
addP.m_B = p.m_B + p1.m_B;
return addP;
}
//运算符重载的函数重载
Person operator+(Person& p, int a) {
Person addP;
addP.m_A = p.m_A + a;
addP.m_B = p.m_B + a;
return addP;
}
void main() {
Person p;
p.m_A = 20;
p.m_B = 11;
Person p1;
p1.m_A = 30;
p1.m_B = 9;
Person p2 = p + p1;
cout << "p2的m_A:" << p2.m_A << "p2的m_B:" << p2.m_B << endl;//m_A:50 m_B:20
Person p3 = p + 10;
cout << "p3的m_A:" << p3.m_A << "p3的m_B:" << p3.m_B << endl;//m_A:30 m_B:21
}
注意:
- 运算符重载也可以发生函数重载(根据传入的参数类型不同,运算符重载的作用也不同)
- 对于内置的数据类型的表达式的运算符是不可能改变的(必须是自定义的数据类型)
左移运算符重载
作用:可以输出自定义的数据类型
注意:我们通常不会使用成员函数重载左移运算符,因为无法实现cout在左侧(因为要用自动自定义的对象调用导致自定义的对象始终在左边,也就是p<<cout)
通过全局函数重载左移运算符
注意:我们想要cout在左边,自定义对象在右边,因此cout在第一个参数,自定义对象在第二个参数
#include <iostream>
using namespace std;
class Person {
public:
int m_A;
int m_B;
};
//等价于只是重写cout<<Person;
//return cout来实现链式访问
ostream& operator<<(ostream& cout,Person& p) {
cout << "m_A:" << p.m_A << "\nm_B:" << p.m_B;
return cout;
}
void main() {
Person p;
p.m_A = 20;
p.m_B = 30;
cout << p << endl;
}
递增运算符重载
作用:可以通过递增运算符重载,进而实现自己的整形数据
前置++:MyInteger& operator++();
后置++:MyInteger operator++(int);
注意:参数列表中的int参数用来区分前置++还是后置++(int在这里是占位符的意思)
#include <iostream>
using namespace std;
class MyInteger {
friend ostream& operator<<(ostream& cout, MyInteger a);
public:
MyInteger() {
m_Num = 1;
}
//重载前置++运算符,返回引用是为了一直对一个对象进行操作
MyInteger& operator++() {
//前置++需要先进行++运算,再将自身做一个返回
m_Num++;
return *this;
}
//重载后置++运算符
//加上int参数,那么编译器就会文伟这是后置运算符的重载了
//后置++不可以返回值为引用类型,因为是临时的值,用完后就会释放
MyInteger operator++(int) {
//记录当前结果
MyInteger temp = *this;
//后递增
m_Num++;
//最后将记录的结果返回
return temp;
}
private:
int m_Num;
};
//重载左移运算符
ostream& operator<<(ostream& cout, MyInteger a) {
cout << "m_Num:" << a.m_Num;
return cout;
}
void main() {
MyInteger myInt;
cout << myInt++ << endl;//1
cout << ++myInt << endl;//3
}
注意:前置递增返回的是引用,后置递增返回的是值
赋值运算符重载
C++编译器至少会给一个类添加4个函数
- 默认的构造函数(无参构造)
- 默认的析构函数(里面不能有参数)
- 默认的拷贝构造函数,会对属性内的值进行拷贝
- 赋值运算符operator=来对属性进行值拷贝
说明:p1=p2 <=> p1.operator=(p2)
#include <iostream>
using namespace std;
class Person {
public:
Person(int age) {
m_Age = new int(age);
}
//浅拷贝带来问题,堆区内存重复释放
~Person() {
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
}
//重载赋值运算符
Person& operator=(Person& p) {
//先判断是否有属性在堆区,若有先释放干净再深拷贝
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
//深拷贝
m_Age =new int(*p.m_Age);
return *this;
}
int* m_Age;
};
void main() {
Person p1(18);
cout << "p1的年龄为:" << *(p1.m_Age) << endl;
Person p2(20);
//若不进行赋值运算符重载则为浅拷贝
p2 = p1;
cout << "p2的年龄为:" << *(p2.m_Age) << endl;
}
关系运算符重载
作用:重载关系运算符,可以让两个自定义类型的对象进行对比操作
重载==号
说明:p1==p2 <=> p1.operator==(p2)
#include <iostream>
using namespace std;
class Person {
public:
Person(string name,int age) {
m_Name = name;
m_Age = age;
}
//重载==号
bool operator==(Person& p) {
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
return true;
}
else {
return false;
}
}
string m_Name;
int m_Age;
};
void main() {
Person p1("lili",18);
Person p2("lili",18);
if (p1 == p2) {
cout << "p1和p2是相等的" << endl;
}
else {
cout << "p1和p2是不相等的" << endl;
}
}
函数调用运算符重载
前言:
- 函数调用运算符()也可以进行运算符重载
- 由于重载后的使用方式非常类似于函数的调用,因此也称为仿函数
- 仿函数没有固定写法,非常灵活
- 仿函数后面也可以接收返回值
说明:对象(参数) <=> 对象.operator()(参数)
具体案例
#include <iostream>
using namespace std;
class MyAdd {
public:
//重载函数调用运算符
int operator()(int a,int b){
return a + b;
}
};
void main() {
MyAdd myAdd;
int add=myAdd(100, 200);
cout << "add:" << add << endl;
//匿名函数对象
cout << "add:" << MyAdd()(200,600) << endl;
}
继承
类与类之间的继承关系
总结:我们发现下级别的成员除了具有上一级的特性,同时还有自己的特性,这个时候我们就考虑用继承
继承语法:
class 子类 : 继承方式 父类{
子类特有的代码;
}
注意:这里面的父类也称基类,主要是子类所复用的类,这里的子类也称为派生类
继承的经典案例
#include <iostream>
using namespace std;
class Father {
public:
void walk() {
cout << "walk long" << endl;
}
private:
void jump() {
cout << "jump up" << endl;
}
};
class Child:public Father {
public:
void run() {
cout << "run fast" << endl;
}
};
void main() {
Child c;
c.run();
c.walk();
}
总结:
- 子类具有父类的所有属性资源(父类的私有资源不可访问),同时也可以有自己独立的资源
- 继承的好处:减少重复的代码,提高代码的复用
继承的方式种类
- 公共继承
- 保护继承
- 私有继承
理解结构图
总结:
- 对于父类私有成员,不管子类用哪种继承方式,子类都不可访问
- 对于公共继承方式,子类按照父类的属性修饰符来继承该属性
- 对于保护继承,子类将父类的所有非私有属性按照保护权限修饰符的方式进行继承
- 对于私有继承,子类将父类的所有属性按照私有属性修饰符的方式进行继承
继承中的对象属性
#include <iostream>
using namespace std;
class A {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class B:public A {
public:
int m_D;
};
void main() {
B b;
//B对象的大小为:16
cout << "B对象的大小为:" << sizeof(B) << endl;
}
总结:子类继承了父类的所有非静态成员属性,当然也包括父类的私有属性,只是父类的私有属性子类不可见
继承中构造和析构的顺序
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "父类构造函数" << endl;
}
~A() {
cout << "父类析构函数" << endl;
}
};
class B:public A {
public:
B() {
cout << "子类构造函数" << endl;
}
~B() {
cout << "子类析构函数" << endl;
}
};
void main() {
B b;
}
总结:构造子类对象时会先构造他的父类对象后再构造自身对象,清理对象时会先清理自身对象再清理父类对象(主要原因:父类属性被子类所复用,父类属性是子类属性的一部分,必须要保证子类属性的完整性)
继承同名成员处理方式
问题:当子类与父类出现同名成员,如何通过子类对象访问到子类或父类中的同名数据呢
#include <iostream>
using namespace std;
class A {
public:
int m_A = 100;
void sayHello() {
cout << "A-hello" << endl;
}
void sayHello(string a) {
cout << "A-hello"<<a << endl;
}
};
class B:public A {
public:
int m_A = 200;
void sayHello() {
cout << "B-hello" << endl;
}
};
void main() {
B b;
//访问子类同名成员,直接访问即可
cout << "子类的m_A=" << b.m_A << endl;//200
b.sayHello();
//访问父类同名成员,需要添加作用域
cout << "父类的m_A=" << b.A::m_A << endl;//100
b.A::sayHello();
b.A::sayHello(" world");
}
注意:
- 子类对象可以直接访问到子类中的同名成员(子类对象.同名属性名)
- 子类对象加作用域可以访问到父类的同名成员(子类对象.父类::同名属性名)
- 若子类中出现了和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的同名成员函数(包括有参数和无参数的函数),若想要访问到父类中被隐藏的同名成员函数,那么需要加上作用域
继承中同名静态成员的处理方法
前言:静态成员和非静态成员出现同名,处理方式一致
#include <iostream>
using namespace std;
class A {
public:
static int m_A;
static int m_B;
static void sayHello() {
cout << "A-hello" << endl;
}
static void sayHello(string a) {
cout << "A-hello"<<a << endl;
}
};
//类外初始化
int A::m_A = 100;
int A::m_B = 500;
class B:public A {
public:
static int m_A;
static void sayHello() {
cout << "B-hello" << endl;
}
};
int B::m_A = 200;
void main() {
B b;
//访问子类同名成员,直接访问即可
cout << "子类的m_A=" << b.m_A << endl;//200
cout << "子类的m_A=" << B::m_A << endl;//200
//子类访问父类的静态属性
cout << "父类的m_B=" << B::m_B << endl;//500
b.sayHello();
//访问父类同名成员,需要添加作用域
cout << "父类的m_A=" << b.A::m_A << endl;//100
cout << "父类的m_A=" << B::A::m_A << endl;//100
b.A::sayHello();
b.A::sayHello(" world");
}
总结:
- 子类可以直接访问父类的静态成员属性(我的理解:静态成员属于类资源,而子类属于父类)
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
- 若子类中出现了和父类同名的成员静态函数,子类的同名成员静态函数会隐藏掉父类中所有的同名成员静态函数(包括有参数和无参数的函数),若想要访问到父类中被隐藏的同名成员静态函数,那么需要加上作用域
- 同名静态成员处理方式和静态成员处理方式一样,只不过有两种访问方式(通过对象和通过类名)
C++中的多继承
前言:在C++中允许一个类继承多个类
语法:
class 子类 : 继承方式 父类1,继承方式 父类2{
子类特有的代码;
}
注意:多继承可能会引发父类中的同名成员出现,需要加作用域区分
#include <iostream>
using namespace std;
class A {
public:
int m_A = 10;
};
class B{
public:
int m_A = 20;
};
class C :public B, public A {
public:
int m_A = 30;
};
void main() {
C c;
cout << "c对象的大小为:" << sizeof(c) << endl;//12
cout << "A中m_A的值为:" << c.A::m_A << endl;
cout << "B中m_A的值为:" << c.B::m_A << endl;
cout << "C中m_A的值为:" << c.m_A << endl;
}
菱形继承
含义:两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被称为菱形继承或者钻石继承
经典案例
菱形继承带来的问题
- 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时会产生二义性(比如究竟使羊的动物属性A还是使用驼的动物属性A——这种可以通过作用域区分)
- 羊驼继承来自动物的数据继承了两份,其实我们应该清楚,这样的数据我们只需要一份即可
虚继承
前言:
- 在继承前(public前)加关键字virtual变为虚继承来解决菱形继承出现的多份数据问题
- 被虚继承的类称为虚基类
- 当发生虚继承之后,那么子类所拥有的相同父类的虚基类属性(该属性必须是从虚基类上继承下来的,不能是自己的)将只存在一份
#include <iostream>
using namespace std;
class Animal {
public:
int m_A = 10;
};
class Sheep :virtual public Animal {};
class Tuo :virtual public Animal {};
class YangTuo :public Sheep, public Tuo {};
void main() {
YangTuo n;
//不是虚继承时
cout << "羊驼访问羊的m_A数据:" << n.Sheep::m_A << endl;
cout << "羊驼访问驼的m_A数据:" << n.Tuo::m_A << endl;
//虚继承时
cout << "羊驼访问羊的m_A数据:" << n.m_A << endl;
}
注意:当菱形继承出现时,两个父类拥有相同的数据,所以要访问特定的父类数据应该加作用域加以区分,但是若是虚继承,所继承的父类虚基类的属性将只存在一份,可以直接通过子类对象访问
多态
多态的理解:一个事物的多种形态
多态的分类
- 静态多态:函数重载和运算符重载(复用函数名)
- 动态多态:派生类和虚函数实现运行时多态
虚函数:在父类的函数的返回值类型前加virtual关键字,那么父类对应的函数就会变成一个虚函数,对应的函数地址可以实现晚绑定
静态多态和动态多态的区别
- 静态多态的函数地址早就绑定——编译阶段确认函数的地址
- 动态多态的函数地址晚绑定——运行阶段确认函数地址
动态多态的满足条件
- 有继承关系
- 子类重写父类的虚函数
动态多态的使用:父类的指针或者引用指向子类的对象
函数重写:子类重写父类的函数,其函数的返回值类型,函数名与参数列表完全相同
案例分析
#include <iostream>
using namespace std;
class Animal {
public:
//函数地址早绑定
void say() {
cout << "动物在说话" << endl;
}
//虚函数,可以实现函数地址晚绑定
virtual void run() {
cout << "动物在疾跑" << endl;
}
};
class Cat:public Animal {
public:
void say() {
cout << "猫在喵喵叫" << endl;
}
void run() {
cout << "小猫在散步" << endl;
}
};
//执行说话的函数
void doWork(Animal& animal) {
animal.say();//动物在说话
animal.run();//小猫在散步
}
void main() {
Cat cat;
doWork(cat);
}
注意:
- 在C++中允许父子之间的类型转换(父类引用指向子类对象)
- 对于父类普通函数的话,用父类引用指向子类对象。函数地址早绑定,在编译阶段就已经确认了函数地址,若调用对应的函数将会是父类的函数
- 对于父类的虚函数(函数返回值类型前加关键字virtual),用父类引用指向子类对象。函数地址晚绑定,函数地址在运行阶段确认,若调用对应的函数将会是子类重写父类对应的函数
- C++规定,当一个函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,因此在子类中重新声明该虚函数时,virtual关键字可以加也可以不加。
动态多态的原理剖析
#include <iostream>
using namespace std;
class Animal1 {
public:
void say() {
cout << "动物在说话" << endl;
}
};
class Animal2{
public:
virtual void say() {
cout << "动物在说话" << endl;
}
};
void main() {
cout << "Animal1所占内存空间:" << sizeof(Animal1) << endl;//1
cout << "Animal2所占内存空间:" << sizeof(Animal2) << endl;//4
}
解释:因为成员函数的存储并不在类的对象上,所以Animal1为一个空对象,所占内存大小为1,而Animal2所占内存空间大小为4,其实Animal2中的4字节空间为一个vfptr(虚函数表指针)该vfptr会指向一个虚函数表(vftable);表的内部存放的是Animal1的虚函数地址(&Animal1::say);当Cat继承了Animal1后,那么便将父类的所有属性均拿过来一份,也就有了父类的那个虚拟函数表指针vfptr,该指针指向了子类的虚函数表(vftable)当子类重写了父类虚函数方法,那么子类的方法将会将子类的虚函数表中的(父类原有的)方法进行覆盖;当父类引用指向子类对象Animal1& animal=cat对象(右值里面有子类的虚函数表);然后在通过animal调用对应的虚函数,将会调用子类虚函数表中的虚函数(因为右值赋予左面的变量)。
纯虚函数和抽象类
前言:在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类的重写内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;
注意:当类中有了纯虚函数(只要有一个),那么这个类也称抽象类
抽象类特点
- 抽象类无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
前言:在多态使用时,若子类中有属性开辟到了堆区,那么父类指针在释放时无法调用子类的析构代码
解决方式:将父类中的析构函数改为虚析构或纯虚析构
虚析构和纯虚析构共性与区别
虚析构和纯虚析构共性
- 可以解决父类指针释放子类对象
- 都需要具体的实现
虚析构和纯虚析构区别
- 若父类中的的析构函数是纯虚析构,那么该类也属于抽象类,无法实例化对象
语法:
虚析构:virtual ~类名(){}
纯虚析构:virtual ~类名()=0;
纯虚析构类外实现:类名::~类名(){}
注意:纯虚析构需要声明,也需要实现(类外实现),因为由于内存的释放导致析构函数会用到
经典案例
#include <iostream>
using namespace std;
class Animal {
public:
//纯虚函数
virtual void say() = 0;
//纯虚析构函数
virtual ~Animal() = 0;
};
//纯虚析构函数实现
Animal::~Animal() {
cout << "Animal的纯虚析构函数" << endl;
}
class Cat :public Animal {
public:
~Cat() {
if (m_Name != NULL) {
cout << "Cat的析构函数调用了" << endl;
delete m_Name;
m_Name = NULL;
}
}
Cat(string name) {
m_Name = new string(name);
}
virtual void say() {
cout << *m_Name<<"小猫喵喵的叫!" << endl;
}
string* m_Name;
};
void main() {
Animal* animal = new Cat("Tom");
animal->say();
delete animal;
}
理解:在堆区中通过父类指针指向子类对象,那么父类的指针在释放时(delete释放指针所在的内存空间)无法调用到子类的析构代码,只会调用到父类的析构代码,这时,只要你将父类的析构函数改为虚析构或纯虚析构,那么delete语句调用的时候便会因为多态的重写原理调用到子类的析构函数,但是由于析构对象时会先析构自身对象再析构父类对象,最终导致子类以及父类对象的析构函数都调用了。
总结:
- 虚析构或纯虚析构就是用来解决父类指针释放子类对象
- 若子类中没有堆区数据,可以不写为虚构函数或纯虚析构
- 拥有纯虚析构函数的类也是抽象类