1 类和对象
C++面向对象的三大特性:封装、继承、多态。
任何物体都有对象,对象有属性和行为功能
1.1 封装
1.1.1 封装的意义
封装是C++面向对象的三大特性之一
封装的意义:
-
将属性和行为作为一个整体,表现为生活中事物
-
将属性和行为加以权限控制
public 类内可访问,类外也可访问
protected 类内可访问,类外不可访问(子类可访问)
private 类内可访问,类外不可访问(子类不可访问)
一般将成员属性设置为私有权限
优点:可以自己控制读写权限;对于写可以检测数据的有效性
1.1.2 struct和class的区别
-
struct的默认权限是public
-
class的默认权限是private
1.1.3 封装案例
1、设计立方体类(Cube),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
#include <iostream>
using namespace std;
class Cube
{
private:
int c;
int k;
int g;
public:
Cube(int c, int k, int g)
{
this->c = c;
this->k = k;
this->g = g;
}
int minaji()
{
return ((this->c * this->k) + (this->c * this->g)
+ (this->g * this->k))* 2;
}
int tiji()
{
return this->c * this->g * this->k;
}
void is_deng(Cube c1 , Cube c2)
{
if (c1.c = c2.c && c1.g == c2.g && c1.k == c2.k)
cout << "两个立方体完全相同" << endl;
else
cout << "两个立方体不完全相同" << endl;
}
};
void Is_deng(Cube c1, Cube c2);
int main()
{
Cube c1(10, 10, 10);
Cube c2(10, 10, 10);
cout << "c1的体积和面积:"<<c1.tiji()<<"\t"<<c1.minaji ()<< endl;
cout << "c2的体积和面积:" << c2.tiji()<<"\t"<< c2.minaji() << endl;
cout << "全局函数判断结果:" << endl;
Is_deng(c1, c2);
cout << "成员函数判断结果:" << endl;
c1.is_deng(c1, c2);
return 0;
}
void Is_deng(Cube c1, Cube c2)
{
if (c1.minaji() == c2.minaji() && c1.tiji() == c2.tiji())
cout << "两个立方体完全相同" << endl;
else
cout << "两个立方体不完全相同" << endl;
}
1.2 对象的初始化和清理
C++中的面向对象来源于生活,每个对象也都有初始化设置及其在对象销毁前的清理数据设置。
1.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
-
**构造函数:**主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
-
**析构函数:**主要作用在于对象销毁前系统自动调用,执行一 些清理工作。
构造函数语法:类名(){}
1、构造函数,没有返回值
2、函数名称与类名相同
3、构造函数可以有参数,因此可发生重载
4、程序在调用对象时会自动调用构造函数,无须手动调用,且只调用一次
析构函数语法:~类名(){}
1、析构函数,没有返回值
2、函数名称与类名相同,在名称之前加~
3、析构函数不可以有参数,因此不可发生重载
4、程序在对象调用前会自动调用析构函数,无须手动调用,且只调用一次
1.2.2 构造函数的分类与调用
两种分类方式:
1、按参数分为:有参构造和无参构造
2、按类型分为:普通构造和拷贝构造
补充:拷贝构造函数语法示例:Person(const Person &p){}
作用:将传入的人身上的所有属性,拷贝到当前对象身上。
(const和引用的作用是为了保护被拷贝对象本身不会被更改)
三种调用方式:
括号法:类名 对象(参数)
显示法:类名 对象 = 类名(参数)
等号后相当于创建了一个匿名对象
隐式转换法:类名 对象 = 参数
相当于类名 对象 = 类名(参数)
1.2.3 拷贝构造函数的调用时机
C++中拷贝构造调用时机通常有三种情况:
- 使用一个已经创建完毕的对象初始化一个新的对象
- 以值传递的方式给函数参数传值
- 以值方式返回局部对象
1.2.4 拷贝构造函数的调用规则
默认情况下,C++编译器至少给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,用于拷贝对象属性值
*·**需要注意的是:*如果定义了有参构造函数或者拷贝构造函数,如果在堆区申请空间时,编译器会调用默认的构造函数。所以如果定义了有参构造函数或者拷贝构造函数后,涉及到堆区的空间,还需要在定义无参构造函数的空实现。
构造函数调用规则:
- 如果自定义有有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造函数
- 如果自定义拷贝构造函数,C++不再提供其他构造函数
1.2.5 深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作(编译器默认提供) 注:会造成堆区的内存重复释放
- 深拷贝:在堆区重新申请空间,进行拷贝操作 可解决浅拷贝带来的问题,重新开辟堆区空间
1.2.6 静态成员
静态成员函数特点:
程序共享一个函数
静态成员函数只能访问静态成员遍历
静态成员函数访问方式:1、对象.函数名()
2、类名::函数名()
1.3 C++对象模型和this指针
1.3.1 成员变量和成员函数分开存储
补充:空对象占用的内存空间为:1,为了区分空对象占用内存的位置
-
非静态成员变量,属于类的对象上
-
非静态成员函数,不属于类的对象上
-
静态成员变量或函数,不属于类的对象上
1.3.2 this指针
通过上面的内容我们知道在C+ +中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例, 也就是说多个同类型的对象会共用一块代码
那么问题是:这- -块代码是如何区分那个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针, 解决主述问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
-
当形参和成员变量同名时,可用this指针来区分
-
在类的非静态成员函数中返回对象本身,可使用
return *this
**this指针的本质:**是一个指针常量,指针的指向不可修改
示例:Person * const this
1.3.3 const修饰成员函数
常函数:
- 成员函数后加const后,此函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时添加mutable关键字后,在常函数中可修改
常对象:
- 在声明对象前添加const关键字后,则对象为常对象
- 常对象只能调用常函数
1.4 友元
友元是指在类中虽然有private权限的内容,既不可被别的类访问也不可在类外被访问,但是通过友元的技术,可以进行特权访问。
友元的关键字:friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
1.4.1 全局函数做友元
在被作为友元类中提前声明,声明语法:friend 返回类型 函数名(参数);
1.4.2 类做友元
在被作为友元类中提前声明,声明语法:friend class 类名;
1.4.3 成员函数做友元
在被作为友元类中提前声明,声明语法:friend 返回类型 类名::函数名(参数);
1.5 运算符重载
1.5.1 加号运算符重载
- 成员函数重载
#include <iostream>
#include<math.h>
using namespace std;
class Person
{
public:
int age;
int id;
Person operator+(Person& p)
{
Person temp;
temp.age = this->age + p.age;
temp.id = this->id + p.id;
return temp;
}
};
int main()
{
Person p1;
p1.age = 10;
p1.id = 1;
Person p2;
p2.age = 10;
p2.id = 2;
Person p3;
p3 = p1 + p2; //等价于p1.operator+(p2)
cout << p3.age << endl;
cout << p3.id << endl;
return 0;
}
- 全局函数重载
#include <iostream>
#include<math.h>
using namespace std;
class Person
{
public:
int age;
int id;
};
Person operator+(Person& p1, Person&p2)
{
Person temp;
temp.age = p1.age + p2.age;
temp.id = p1.id + p2.id;
return temp;
}
int main()
{
Person p1;
p1.age = 10;
p1.id = 1;
Person p2;
p2.age = 10;
p2.id = 2;
Person p3;
p3 = p1 + p2; //等价于p3 = operator+( p1, p2)
cout << p3.age << endl;
cout << p3.id << endl;
return 0;
}
1.5.2 左移运算符重载
#include <iostream>
#include<math.h>
using namespace std;
class Person
{
public:
int age;
int id;
ostream& operator<<(ostream& cout) //因为目标所需的格式为cout<<p , 但是如果使用类内实现等价于p.operator<<(cout) 等价于p<<cout 因此重载<<不在类内实现 {
cout << "age:" << this->age << endl;
cout << "id:" << this->id << endl;
return cout;
}
};
ostream& operator<<(ostream &cout, Person&p) //cout类型为ostream,因为只能有一个cout关键字,所以使用引用
{
cout << "age:" << p.age << endl;
cout << "id:" << p.id << endl;
return cout; //这里返回cout是为了可以继续追加<<运算符, 连续执行
}
int main()
{
Person p;
p.age = 10;
p.id = 1;
cout << p << endl; //等价于 operator<<(operator<<(cout, p), endl)
return 0;
}
其他运算符重载与上述类似在此不再赘述
1.6 继承
继承是面向对象的三大特性之一
继承是指大类分出的小类,不仅有自己的特点还具有大类的特点。
1.6.1 继承的基本语法
class 子类 : 访问方式 基类
例:class A : public B;
A类称为子类或者派生类
B类称为父类或者基类
继承的作用:可以减少重复的代码
派生类中的成员,包含两大部分:
1、一类是从基类继承过来的,一类是自己增加的成员。
2、从基类继承过过来的表现其共性,而新增的成员体现了其个性。
1.6.2 继承方式
一共有三种继承方式:
- 公共继承 public
- 保护继承 protected
- 私有继承 private
1.6.3 继承中的对象模型
子类在继承基类的属性时,会继承所有的属性,而对于private下的属性同样继承只是隐藏起来不显示
利用开发人员命令提示工具查看对象模型
跳转盘符 F:
跳转文件路径 cd 具体路径下
查看命名
cl /d1 reportSingleClassLayout类名 文件名
1.6.4 继承中构造和析构顺序
**顺序:**父类构造函数---->子类构造函数---->子类析构函数---->父类析构函数
1.6.5 继承中同名成员的处理方式
问题:当子类与父类中出现同名的成员,如何通过子类对象,访问到子类或父类中的同名成员呢?
-
访问子类同名成员 直接访问即可 (语法:
子类.同名成员
) -
访问父类同名成员 需要添加作用域(语法:
子类.父类::同名成员
)注意:如果子类中出现父类同名的成员函数,子类的同名函数会隐藏掉父类中所有同名函数,所有需要添加作用域
1.6.6 继承中同名静态成员的处理方式
**问题:**继承中同名的静态成员在子类对象上如何进行访问?
1、通过对象访问
- 访问子类同名成员 直接访问即可 (语法:
子类.同名成员
) - 访问父类同名成员 需要添加作用域(语法:
子类.父类::同名成员
)
2、通过类名访问
- 访问子类同名成员 直接访问即可 (语法:
子类::同名成员
) - 访问父类同名成员 需要添加作用域(语法:
子类::父类::同名成员
)
1.6.7 多继承
C++中允许一个类继承多个类
语法:class 子类 : 继承方式 父类1, 继承方式 父类2...
因为使用多继承时,常会导致出现多个同名成员,所以不建议使用多继承
1.6.8 菱形继承
菱形继承:指的是两个子类同继承自一个基类,然后又出现了一个子类同时继承于前两个子类
例:马和驴继承于动物类,骡子又继承于马和驴
出现的问题:
-
当两个父类有相同的成员,子类在访问同名成员时需要加作用域
-
当出现1的情况时,子类继承了两份相同的成员,造成了资源浪费
解决方法:利用虚继承
语法:class 子类 :
virtual 访问方式 父类
未采用虚继承:
采用虚继承后:
使用了虚继承后SheepTuo没有继承Sheep和Tuo的全部属性,仅继承了两者的vptr(虚指针),而vptr指向vbtable(vbtable中记录了获取SheepTuo继承自Sheep和Tuo类所需偏移的地址值:Sheep需要偏移8、Tuo需要偏移4)
1.7 多态
1.7.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态:函数重载 和 运算符重载,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
示例:
#include <iostream>
#include<math.h>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在叫" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在叫" << endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout << "小狗在叫" << endl;
}
};
class DogSon : public Dog
{
public:
void speak()
{
cout << "狗儿子在叫" << endl;
}
};
void test(Animal& animal) //相当于 Animal &animal = cat 注:父类到子类不需要类型转化
{
animal.speak();
}
int main()
{
DogSon DS;
test(DS);
return 0;
}
输出结果:
狗儿子在叫
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 — 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 — 运行阶段确定函数地址
多态的满足条件:
-
有继承关系
-
子类重写父类中的虚函数
注:重写和重载的区别:重载是函数的名称相同参数不同;重写是指函数名、返回值、参数都相 同
多态的使用条件:
- 父类指针或引用指向子类对象 例:
Animal * animal = new Cat
或Animal & animal = Cat cat
多态的优点:
- 组织结构清晰
- 可读性强
- 对于前期和后期的扩展和维护性高
1.7.2 多态的原理
当子类重写父类的虚函数时,vftable中的原地址替换为子类中重写后的函数地址
在开发人员命令行下显示:
1、若父类同名函数前未声明virtual关键字,内部结构如下图所示
因为此时类的内部什么都没有,为空类,所以大小为1
2、若父类同名函数前声明virtual关键字,内部结构如下图所示
生成了一个vfptr指针,所以大小为4,vfptr指向vftable,存放函数地址
3、当子类没有重写父类中的speak函数时,子类的内部结构如下图所示
因为Cat类继承了Animal类,所以内部结构完全复刻了Animal中的结构
4、当子类重写父类中的speak函数时,子类的内部结构如下图所示
当Cat类重写了speak函数后vftable中存放的函数地址替换为了重写后的函数地址
1.7.3 纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容
所以可以将虚函数改为纯虚函数
纯虚函数的语法:virtual 返回类型 函数名(参数) = 0;
另外当一个类中有了纯虚函数,这个类也称为抽象类
抽象类的特点:
1、无法实现实例化对象
2、子类必须重写抽象类的纯虚函数,否则也属于抽象类
1.7.4 虚析构和纯虚析构
多态使用时,如果是使用父类指针指向子类对象使用多态时,例:Animal * animal = new Cat
,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
类内:virtual ~类名() = 0;
类外:类名::~类名(){}
虚析构和纯虚析构共性:
- 都可以解决父类指针释放子类对象的问题
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
为了重写后的函数地址*
1.7.3 纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容
所以可以将虚函数改为纯虚函数
纯虚函数的语法:virtual 返回类型 函数名(参数) = 0;
另外当一个类中有了纯虚函数,这个类也称为抽象类
抽象类的特点:
1、无法实现实例化对象
2、子类必须重写抽象类的纯虚函数,否则也属于抽象类
1.7.4 虚析构和纯虚析构
多态使用时,如果是使用父类指针指向子类对象使用多态时,例:Animal * animal = new Cat
,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
类内:virtual ~类名() = 0;
类外:类名::~类名(){}
虚析构和纯虚析构共性:
- 都可以解决父类指针释放子类对象的问题
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类