7.22
1、内存分区模型
C++ Primer Plus(嵌入式公开课)—第4章 复合类型–>4.8.5 自动存储、静态存储和动态存储
C++程序在执行时,将内存大方向划分为4个区域
①代码区:存放函数体的二进制代码,由操作系统OS进行管理的。
②全局区:存放全局变量
和静态变量(static)
以及常量(全局常量+字符串常量)
。
③栈区:由编译器自动分配释放, 存放函数的形参
,局部变量(包括局部常量)
等。
④堆区:由程序员分配和释放(new出来的东西),
,若程序员不释放,程序结束时由操作系统回收。
ps.变量包括普通变量和用const修饰的变量(即常量),全局常量即用const修饰的全局变量,同理局部常量即用const修饰的局部变量。
//存放在栈区:
局部变量a地址为: 3865544
局部变量b地址为: 3865532
局部常量c_l_a地址为: 3865520
局部常量c_l_b地址为: 3865508
//存放在全局区:
全局变量g_a地址为: 13598728
全局变量g_b地址为: 13598732
静态变量s_a地址为: 13598736
静态变量s_b地址为: 13598740
字符串常量地址为: 13587268
字符串常量地址为: 13587284
全局常量c_g_a地址为: 13587072
全局常量c_g_b地址为: 13587076
补充1:
栈区存放函数的形参
,局部变量(包括局部常量)
等。
不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
补充2:new操作符
示例:
int* func()
{
int* a = new int(10);//new一个int型变量,赋上10,并将这个变量的地址返回
return a;
}
int main() {
int a = 20;
int* p = func();
cout << *p << endl;//10
cout << *p << endl;//10
//将指针p指向的堆区的数据int(10)释放掉
delete p;
//cout << *p << endl; p已经通过delete被释放掉就不能再次进行访问
//引发了异常: 读取访问权限冲突。p 是 0x8123。
p = &a;//指针p本身依旧可以重新指向别的变量,上面delete释放的是p刚开始指向的new出来的堆区的数据int(10)
cout << *p << endl;//20
system("pause");
return 0;
}
①int* a = new int(10);//new一个int型变量,赋上10,并将这个变量的地址返回,即new出来的东西要用一个指针来接收
②指针*p本质上是一个局部变量,存放在栈区;而new出来的int(10)存放在堆区
③new一个数组:
int* arr = new int[10];
for (int i = 0; i < 10; i++)
arr[i] = i + 10;
for (int i = 0; i < 10; i++)
cout << arr[i] << " ";
cout << endl;
//释放new出来的数组
delete[] arr;
2、引用
作用:给变量起别名
语法: 数据类型 &别名 = 原名
int a = 10;
int &b = a;//b是别名,原名是a
cout << "a = " << a << endl;//10
cout << "b = " << b << endl;//10
b = 100;
cout << "a = " << a << endl;//100
cout << "b = " << b << endl;//100
注意事项:
①引用必须初始化(&b = a;),并且初始化之后不可改变
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int &c = a; //一旦初始化后,就不可以更改
//&c = b; 更改引用的操作是不合法的
c = b; //这是赋值操作,不是更改引用
cout << "a = " << a << endl;//20
cout << "b = " << b << endl;//20
cout << "c = " << c << endl;//20
②引用的本质是一个指针常量
(int* const p; 指向固定,指向的值可变)
指针常量和常量指针的区别:
指针常量:指针是个常量,即指向固定;---引用
常量指针:指向常量的指针,即指向的值固定。
2.1 引用做函数参数
函数传参时,可以利用引用的技术让形参修饰实参
//值传递
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
//地址传递
void swap2(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//引用传递
void swap3(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main(){
int a = 10, b = 20;
swap1(a, b);
cout << a << "," << b << endl;//10,20
a = 10, b = 20;
swap2(&a, &b);//形参是指针,所以实参要加&
cout << a << "," << b << endl;//20,10
a = 10, b = 20;
swap3(a, b);//传参的时候直接给a和b,不用加&
cout << a << "," << b << endl;//20,10
}
2.2 引用做函数的返回值
int& test01() {//返回一个引用
static int a = 10;//静态变量,存在全局区
return a;
}
int main(){
int& ref = test01();//函数test01()返回的是一个引用&,所以要用一个引用&去接收返回值
//别名是ref,原名是test01()中的a
cout << ref << endl;//10
system("pause");
return 0;
}
2.3 常量引用
常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参
,防止形参改变实参。
void showValue(int& v) {
v++;
cout << v << endl;
}
void showValue1(const int& v) {
//v++;语法错误,因为形参v用const修饰之后就不能再修改了
//也就是通过这种方式来避免由于形参的变化导致实参也被动变化
cout << v << endl;
}
int main(){
int a = 10;
showValue(a);//原名是a,别名是v //a=10给到showValue()后,在函数内别名v自加一,所以结果是11
a = 100;//原名b是可以变化的,但别名v的变化也会导致b的值发生改变
showValue(a);a=100给到showValue()后,在函数内别名v自加一,所以结果是101
int b = 20;
showValue1(b);原名是b,别名是v //20
//由于形参加了个const,所以别名v就成了一个常量,不能再变化,这样就避免了由于v的变化而导致实参b的改变
b = 30;//
showValue1(b);//30
system("pause");
return 0;
}
3、函数提高
3.1 函数默认参数
//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
int func(int a, int b = 10, int c = 10) {
return a + b + c;
}
//2. 如果函数声明时有默认值,那么函数定义的时候就不能有默认参数
//声明:
int func2(int a = 10, int b = 10);//声明中给出默认值
//定义:
int func2(int a , int b ) {//定义中不允许再给出默认值
return a + b;
}
int main(){
//函数默认参数
//如果我们自己传入数据,就用自己的数据,如果没有,就用默认值
//cout << "ret = " << func() << endl;//参数a没有默认值
cout << "ret = " << func(100) << endl;//100+10+10=120
cout << "ret = " << func(20, 20) << endl;//20+20+10=50
cout << "ret = " << func(10,10,10) << endl;//10+10+10=30
cout << func2() << endl;//20
cout << func2(20) << endl;//20+10=30
system("pause");
return 0;
}
3.2 函数重载
作用:函数名可以相同,提高复用性
3.2.1 函数重载满足条件:
① 同一个作用域下
②函数名称相同
③函数参数的 类型不同
、个数不同
、顺序不同
//函数重载需要函数都在同一个作用域下
void func()
{
cout << "func 的调用!" << endl;
}
void func(int a)
{
cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a ,int b)
{
cout << "func (double a ,int b)的调用!" << endl;
}
//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
// cout << "func (double a ,int b)的调用!" << endl;
//}
int main() {
func();
func(10);
func(3.14);
func(10,3.14);
func(3.14 , 10);
system("pause");
return 0;
}
3.2.2 函数重载注意事项:
①引用作为重载条件
②函数重载碰到函数默认参数://碰到默认参数产生歧义,需要避免
//函数重载注意事项
//1、引用作为重载条件
void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}
//2、函数重载碰到函数默认参数
void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}
void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}
int main() {
int a = 10;
func(a); //调用无const
func(10);//调用有const
//func2(10); //碰到默认参数产生歧义,需要避免
system("pause");
return 0;
}
4、类与对象
在设计类的时候,属性和行为写在一起,表现事物
4.1 封装
4.1.1 语法
class 类名{ 访问权限: 属性 / 行为 };
补充:
类(抽象)---对象(具体)
属性(变量)
行为(函数/方法)
4.1.2 访问权限
//三种权限:
//公共权限 public 类内可以访问 类外可以访问
//保护权限 protected 类内可以访问 类外不可以访问
//私有权限 private 类内可以访问 类外不可以访问
class Person
{
//姓名 公共权限
public:
string m_Name;
//汽车 保护权限
protected:
string m_Car;
//银行卡密码 私有权限
private:
int m_Password;
public:
void func()
{
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
}
};
int main() {
Person p;
p.m_Name = "李四";
//p.m_Car = "奔驰"; //保护权限类外访问不到
//p.m_Password = 123; //私有权限类外访问不到
system("pause");
return 0;
}
4.1.3 类class和结构体struct的区别
在C++中 struct和class唯一的区别就在于默认的访问权限不同
:
* struct 默认权限为公共
* class 默认权限为私有
示例:
class C1
{
int m_A; //默认是私有权限
};
struct C2
{
int m_A; //默认是公共权限
};
int main() {
C1 c1;
c1.m_A = 10; //错误,访问权限是私有
C2 c2;
c2.m_A = 10; //正确,访问权限是公共
system("pause");
return 0;
}
4.1.4 案例
判断两个立方体是否相同:
1.Cube类中的成员变量(L W H)设置为private,每个变量对应的设置读取函数(setL()和getL())。这是因为成员变量是私有权限,所以在类之外例如main函数中就无法访问到L W H,只能通过setL()和getL()来设置和获取L的值。
2.成员函数calArea()可以不要形参,直接用类内的成员变量(L W H)
3.判断两个立方体是否相同:
成员函数即类内的函数 bool isSame(Cube c2);
全局函数isSame(Cube c1,Cube c2)
4.//测试return
else这行可以不要,但最后一句return 0不能少!!!
bool testReturn() {
int a = 10, b = 11;
if (a == b)
return 1;
else//不要这个else也行,因为只要满足a == b ,执行了上一行的return 1,这个函数就结束了,不会到return 0,所以可以不要这个else
return 0;//但是这个return 0不能少,否则不论是否满足a == b,都会执行return 1,因为只有return 1这一个出口
}
//案例二:点和圆的关系
利用头文件和源文件使得程序更有条理:
首先Point.h下的程序为:
//类的声明:包括类的成员变量和成员函数的声明
//①下面这三行要有
#pragma once
#include<iostream>
using namespace std;
//判断点和圆的关系---点类
class Point {
private:
double x;//②成员变量的声明
double y;
public:
void setX(double x);//③成员函数的声明
double getX();
void setY(double y);
double getY();
};
然后Point.cpp下的程序:()
//类的实现:类的成员函数的实现
#include"Point.h" //①Point类的头文件的名字
void Point::setX(double x) { //②记得加作用域(类名::)Point::
this->x = x;
}
double Point::getX() {
return x;
}
void Point::setY(double y) {
this->y = y;
}
double Point::getY() {
return y;
}
接下来是Circle.h的程序:
//类的声明,包括类的成员变量和成员函数的声明
#pragma once
#include<iostream>
#include"Point.h" //①因为Circle类中包括了Point类,所以要声明Point类的头文件
using namespace std;
//判断点和圆的关系---圆类
class Circle {
private:
int radius;//半径
Point center;//圆心
public:
void setR(int r);
int getR();
void setCenter(int x, int y);
Point getCenter();
void result(Point p);
};
最后是Circle.cpp的程序:
//类的实现:类的成员函数的实现
//①Circle类的头文件的名字
#include"Circle.h"
void Circle::setR(int r) {//作用域(类名::)
radius = r;
}
int Circle::getR() {
return radius;
}
void Circle::setCenter(int x, int y) {
center.setX(x);
center.setY(y);
}
Point Circle::getCenter() {
return center;
}
void Circle::result(Point p) {
int distance = sqrt((p.getX() - center.getX()) * (p.getX() - center.getX()) + (p.getY() - center.getY()) * (p.getY() - center.getY()));
if (distance > radius)
cout << "点在圆外" << endl;
else if (distance == radius)
cout << "点在圆上" << endl;
else
cout << "点在圆内" << endl;
}
总结:
1.头文件Circle.h中
①要加
#pragma once
#include<iostream>
#include"Point.h" //因为Circle类中包含Point类
using namespace std;
②声明类的成员变量和成员函数
2.源文件Circle.cpp中
①要加头文件声名
#include"Circle.h"
②要加作用域(类名::)Circle::
例如:
void Circle::setR(int r) {
radius = r;
}
4.2 对象的初始化和清理
4.2.1 构造函数与析构函数
class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
int main() {
Person p;
system("pause");
return 0;
}
结果:
构造
请按任意键继续. . .
析构
4.2.2 构造函数的分类及调用
两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
//构造函数分类
class Test1 {
public:
int age;
public:
Test1() {
cout << "无参构造" << endl;
age = 0;//成员变量初始化,否则会提示警告C26495
}
Test1(int age) {
this->age = age;
cout << "有参构造" << endl;
}
Test1(const Test1& p) {//参数是Test1类的引用,并且是一个常量引用,防止形参改变实参,也即形参p不可变
this->age = p.age;//将形参p的所有属性拷贝到“我”身上
cout << "拷贝构造" << endl;
}
~Test1() {
cout << "析构2" << endl;
}
};
三种调用方式:
括号法
显示法
隐式转换法
int main() {
/*构造函数与析构函数*/
Test0 p0;
cout << "\n\n" << endl;
Test1 p1;//创建一个对象的时候就相当于调用了无参构造函数
Test1 p2(10);//调用有参构造函数
Test1 p3(p1);//调用拷贝构造函数
cout << p2.age << endl;//10
cout << p3.age << endl;//0
system("pause");
return 0;
}
4.2.3 拷贝构造函数调用时机
使用一个已经创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值
以值方式返回局部对象
注意:
拷贝构造函数使用时机:1.用已有对象初始化新对象。 2,传参。3,函数返回值。当=的左侧为非新对象时,无法调用拷贝构造函数,此时要拷贝需重载赋值=运算符
4.2.4 构造函数调用规则
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
4.2.5 深拷贝与浅拷贝
浅拷贝(编译器自带的拷贝构造函数):简单的赋值拷贝操作,浅拷贝带来的问题是堆区内存重复释放
深拷贝(手动写的拷贝构造函数):在堆区重新申请空间,进行拷贝操作。
示例:
//深拷贝&浅拷贝
class Person1 {
public:
int age;
int* height;
public:
Person1() {
cout << "无参构造" << endl;
this->age = 0;
*this->height = 0;
}
Person1(int age, int height) {
this->age = age;
//在堆区开辟空间,存放this->height
this->height = new int(height);//this->height是个地址
cout << "this->height地址:" << (int)this->height << endl;//17681832
cout << "有参构造" << endl;
}
Person1(const Person1& p) {
this->age = p.age;
//this->height = p.height;
//开始拷贝之前,在堆区为形参p开辟新的空间,用来存放p.height
this->height = new int(*p.height);//括号里的*p.height是一个值,不是地址
cout << "形参p地址:" << (int)p.height << endl;//17681832
cout << "拷贝构造" << endl;
}
~Person1() {//析构是将堆区开辟的数据进行释放操作
if (this->height != NULL){
delete this->height;
this->height = NULL;
}
cout << "析构" << endl;
}
};
int main()
{
//深拷贝,浅拷贝
Person1 p1(18, 160);//有参构造
Person1 p2(p1);//拷贝构造
cout << "地址p1:" << (int)p1.height << endl;//17681832
cout << "地址p2:" << (int)p2.height << endl;//17681992
***//对象p1在堆区开辟了空间存放身高数据160,返回地址17681832
//对象p2在堆区17681992的位置存放从*(p.height)复制来的身高数据160
//各自在析构的时候delete各自在堆区开辟的空间,而不会导致堆区空间重复释放***
cout << "p1的年龄: " << p1.age << " 身高: " << *p1.height << endl;//18 160
cout << "p2的年龄: " << p2.age << " 身高: " << *p2.height << endl;//18 160
system("pause");
return 0;
}
对上图的解释:
浅拷贝只是简单的将p1.height(0x0011)复制给p2.height,此时两个height指向同一块内存(0x0011);
由于p1和p2是局部变量,保存在栈区,所以遵循先进后出
的原则,程序执行完先对p2进行析构,把p2.height(0x0011)指向的内容(160)进行释放,此时p1.height的指向已经为空,后续对p1进行析构的时候还会再释放一次p1.height(0x0011)所指向的内容,堆区中同一块内存被重复释放两次,导致报错。
4.2.6 静态成员
静态成员分为:静态成员变量和静态成员函数。
- 静态成员变量
所有对象共享同一份数据,数据可以改变
- 在编译阶段分配内存
类内声明,类外初始化
- 静态成员函数
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
空对象占用的空间内存:1字节。
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象
上,静态成员变量和静态成员函数和非静态成员函数都不占对象空间。
4.3.2 this指针—在两个地方有用到(C++第七阶段的补充和C++提高编程2的3.2.7 vector互换容器)
this指针的用途:
①当形参和成员变量同名时,可用this指针来区分
②在类的非静态成员函数中返回对象本身
,可使用return *this
//this指针
class Person4 {
public:
int age;
public:
Person4(int age) {
this->age = age;
}
Person4& personAddAge(Person4 p) {
this->age += p.age;
//返回对象本身
return *this;//返回的是一个地址
}
};
int main(){
//this指针---在类的非静态成员函数中返回对象本身,可使用return *this
Person4 p1(10);
cout << p1.age << endl;//10
cout << p1.personAddAge(p1).age << endl;//10+10=20
cout << p1.age << endl;//20
cout << p1.personAddAge(p1).personAddAge(p1).age << endl;/20+20=40 40+40=80
cout << p1.age << endl;//80
cout << p1.personAddAge(p1).age << endl;//80+80=160
p1.age = 10;
Person4 p2(10);//10
p2.personAddAge(p1).personAddAge(p1).personAddAge(p1);//10+10*3=40
cout << "p2.age = " << p2.age << endl;//40
system("pause");
return 0;
}
4.3.3 const修饰成员函数
4.4 友元
友元的目的就是让一个函数或者类 访问另一个类中私有成员
,关键字是 friend
。
友元的三种实现:
全局函数做友元
类做友元
成员函数做友元
4.4.1 全局函数做友元
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
4.4.2 类做友元
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
4.4.3 成员函数做友元
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
4.5 运算符重载 点这里
4.6 继承
有些类与类之间存在特殊的关系,下级别的成员除了拥有上一级的共性,还有自己的特性
。这个时候我们就可以考虑利用继承
的技术,减少重复代码。
4.6.1 继承的基本语法
class A: public B{
};
A类称为子类或派生类
B类称为父类或基类
示例:
//公共页面
class BasePage {
public:
//公共头部
void header() {
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
//公共底部
void footer() {
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
//公共左栏
void left()
{
cout << "Java,Python,C++...(公共分类列表)" << endl;
}
};
//Java界面:
class Java :public BasePage {//类名后面:public BasePage
public:
void content() {
cout << "JAVA学科视频" << endl;
}
};
int main(){
//Java界面
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();
cout << endl;
system("pause");
return 0;
}
4.6.2 继承方式
继承的语法:
class 子类:继承方式 父类{
};
继承方式分为public,protected,private
公共继承,保护继承,私有继承。
总结:
1.父类中的私有内容(private)任何一种继承方式都访问不到,即无法被访问/被继承;
2.公共继承:父类中的各访问权限不变
3.保护继承:父类中的各访问权限都变成protected保护权限
4.私有继承:父类中的各访问权限都变成private私有权限
4.6.3 继承中的对象模型
父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到。即子类所占内存包括父类的所有内容和自己的内容所占内存之和
。
4.6.4 继承中的构造和析构顺序
继承中 先调用父类构造函数,再调用子类构造函数
,析构顺序与构造相反。
示例:
class Base0 {
public:
Base0() {
cout << "父类构造函数" << endl;
}
~Base0() {
cout << "父类析构函数" << endl;
}
};
class Son0 :public Base0{
public:
Son0() {
cout << "子类构造函数" << endl;
}
~Son0() {
cout << "子类析构函数" << endl;
}
};
int main(){
Son0 son;//通过默认构造函数,创建一个对象
system("pause");
return 0;
}
结果:
父类构造函数
子类构造函数
请按任意键继续. . .
子类析构函数
父类析构函数
4.6.5 继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
//子类与父类出现相同的成员(变量/函数)时,子类对象如何访问到同名的数据
Son1 son;
//同名成员变量
cout << "子类中的a = " << son.a << endl;//直接访问
cout << "父类中的a = " << son.Base1::a << endl;//加父类的作用域
//同名成员函数
son.func();//直接访问
son.Base1::func();//加父类的作用域
son.Base1::func(10);//加父类的作用域
4.6.6 多继承语法
多继承:一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...
C++实际开发中不建议用多继承
4.6.7 菱形继承
概念:
两个派生类继承同一个基类;又有某个类同时继承者两个派生类;这种继承被称为菱形继承,或者钻石继承。
典型案例:
- 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
- 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
问题:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
4.7 多态
4.7.1 多态的基本概念
①多态分为两类:
- 静态多态:
函数重载
和 运算符重载属于静态多态,复用函数名 - 动态多态:
派生类
和虚函数
实现运行时多态
②静态多态和动态多态区别:
- 静态多态的
函数地址早绑定
-编译阶段
确定函数地址 - 动态多态的
函数地址晚绑定
-运行阶段
确定函数地址
③多态满足条件:
- 有继承关系
- 子类
重写
父类中的虚函数(virtual 函数名)
④多态使用条件
- 父类
指针或引用
指向子类对象
⑤重写:函数返回值类型 函数名 参数列表
完全一致称为重写
重载: ① 同一个作用域下
②函数名称相同
③函数参数的 类型不同
、个数不同
、顺序不同
示例:
关键在于父类中成员函数前的virtual关键字
//动物类
class Animals {
public:
//函数前面加上virtual关键字,speak函数就是虚函数
virtual void speak() {
cout << "动物在说话" << endl;
}
};
//猫类
class Cats: public Animals{
public:
void speak() {
cout << "小猫在说话" << endl;
}
};
class Dogs:public Animals {
public:
void speak() {
cout << "小狗在说话" << endl;
}
};
//全局函数
void doSpeak(Animals& ani) {
ani.speak();
}
int main(){
cout << "sizeof Animals类 = " << sizeof(Animals) << endl;//有virtual关键字的Animals类占4字节
//没有virtual关键字的Animals类占1字节,空类,并且非静态成员函数不属于类的内存(见4.3.1 成员变量和成员函数分开存储)
//加了virtual关键字后Animals类占4字节,不再是空类,而是多了一个指针,叫vfptr(虚函数表指针),表内记录虚函数的地址
Cats cat;//当子类 重写 父类的 虚函数 ,子类中的虚函数表内部会替换成子类的虚函数地址
//子类重写父类的虚函数,即子类也有个vfptr(虚函数表指针),表内记录虚函数的地址
cout << "sizeof Cats类 = " << sizeof(Cats) << endl;//4
//当父类的 指针或者引用 指向子类对象的时候,就发生了多态
doSpeak(cat);//小猫在说话
Dogs dog;
doSpeak(dog);//小狗在说话
system("pause");
return 0;
}
多态的底层原理:
首先在父类中的虚函数
(virtual
void Speak(){}),使得父类占4个字节,这4个字节是个vfptr(虚函数表指针)
,它指向虚函数表(vftable)
,表内记录虚函数的地址(&Animal::speak
);
然后是子类重写父类中的虚函数,因此子类也占4个字节,这4个字节也是个vfptr(虚函数表指针),它也指向虚函数表(vftable),表内也记录虚函数的地址;在子类重写父类中的虚函数后,子类中的虚函数表内部会替换成子类的虚函数地址(&Cats::speak)。
最后是当父类的指针或者引用
指向子类对象
时,就发生了多态
。
派生类虚表:
1.先将基类的虚表中的内容拷贝一份
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数
3.如果派生类中新增加自己的虚函数,按照其在派生类中的声明次序,放在上述虚函数之后
原文链接:https://blog.csdn.net/qq_39412582/article/details/81628254
4.7.2 多态案例1—计算器类
//如果想扩展新的功能,需要修改源码
//在真实开发中提倡开闭原则
//开闭原则:对扩展进行开放,对修改进行关闭。
多态技术:
①继承②父类有虚函数③子类重写父类的虚函数
④父类的指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表
完全一致称为重写
程序:
#include<iostream>
#include<string.h>
using namespace std;
//分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
//普通写法
class Calculator {
public:
int a;
int b;
public:
int getResult(string oper) {
if (oper == "+")
return a + b;
else if (oper == "-")
return a - b;
else if (oper == "*")
return a * b;
//如果想扩展新的功能,需要修改源码
//在真实开发中提倡开闭原则
//开闭原则:对扩展进行开放,对修改进行关闭。
}
};
//多态写法:①继承②父类有虚函数③子类重写父类的虚函数④父类的指针(下面第76行)或引用(源.cpp中449行)指向子类对象
//父类:抽象计算器
class abstractCalculator {
public:
int a, b;
virtual int getResult() {//②虚函数
return 0;
}
};
//子类:加法计算器
class addCalculator:public abstractCalculator {//①继承
public:
int getResult() {//③子类重写父类的虚函数
return a + b;
}
};
//子类:减法计算器
class subtractCalculator :public abstractCalculator {//①继承
public:
int getResult() {//③子类重写父类的虚函数
return a - b;
}
};
//子类:乘法计算器
class multipleCalculator :public abstractCalculator {//①继承
public:
int getResult() {//③子类重写父类的虚函数
return a * b;
}
};
//子类:除法计算器
class divideCalculator :public abstractCalculator {//①继承
public:
int getResult() {//③子类重写父类的虚函数
return a / b;
}
};
//全局函数(为了完成④父类的引用指向子类对象)
int doCalculator(abstractCalculator& abc) {
return abc.getResult();
}
int main() {
//普通写法:
Calculator c;
c.a = 10;
c.b = 20;
cout << "相加:" << c.getResult("+") << endl;
cout << "相减:" << c.getResult("-") << endl;
cout << "相乘:" << c.getResult("*") << endl;
//多态写法:
//④父类的指针指向子类对象addCalculator
abstractCalculator* abc = new addCalculator;
abc->a = 10;
abc->b = 12;
cout << "加法:" << abc->getResult() << endl;//22
delete abc;//new出来的内容要记得销毁
//④父类的指针指向子类对象multipleCalculator
abc = new multipleCalculator;//这里的父类指针abc本身依旧可以重新指向别的变量,(见test_new和delete.cpp)
//上面delete释放的是abc刚开始指向的new出来的子类对象addCalculator
abc->a = 6;
abc->b = 5;
cout << "乘法:" << abc->getResult() << endl;//30
delete abc;//这里销毁的是父类指针abc指向的89行new出来的子类对象multipleCalculator
//④父类的引用指向子类对象subtractCalculator
subtractCalculator sub;
sub.a = 11;
sub.b = 13;
cout << "减法:" << doCalculator(sub) << endl;//-2
system("pause");
return 0;
}
4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的现实无意义的,主要都是调用子类重写的内容,所以可以将虚函数改为“纯虚函数
”,纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
父类中有了纯虚函数,这个类就被称为抽象类
。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
程序:
//父类:抽象计算器
class abstractCalculator {
public:
int a = 0, b = 0;
//virtual int getResult() {//②虚函数
// return 0;
//}
virtual int getResult() = 0;//纯虚函数
};
4.7.4 多态案例2—制作饮品
①可以把很多个步骤放到一个非静态成员函数中,这个函数就相当于一个接口
,各个子类都可以通过这个接口进入。
//父类:抽象制作饮品
class abstractMakingDrinking {
public:
//纯虚函数
virtual void boilingWater() = 0;//煮水
virtual void chongPao() = 0;//冲泡
virtual void takeToBottle() = 0;//倒入杯中
virtual void addFlavouring() = 0;//加佐料
//非静态成员函数,相当于一个接口
void makingDrinking() {
boilingWater();
chongPao();
takeToBottle();
addFlavouring();
}
};
//子类:冲咖啡
class makingCoffee :public abstractMakingDrinking {
virtual void boilingWater() {//煮水
cout << "煮水" << endl;
}
virtual void chongPao() {//冲泡
cout << "冲泡咖啡" << endl;
}
virtual void takeToBottle() {//倒入杯中
cout << "倒入杯中" << endl;
}
virtual void addFlavouring() {//加佐料
cout << "加糖和牛奶" << endl;
}
};
//子类:冲茶叶
class makingTee :public abstractMakingDrinking {
virtual void boilingWater() {//煮水
cout << "煮水" << endl;
}
virtual void chongPao() {//冲泡
cout << "冲泡茶叶" << endl;
}
virtual void takeToBottle() {//倒入杯中
cout << "倒入杯中" << endl;
}
virtual void addFlavouring() {//加佐料
cout << "加柠檬" << endl;
}
};
②实现多态的时候,父类的指针/引用
指向子类对象
//全局函数-->①父类的引用指向子类对象
void doMaking(abstractMakingDrinking& mD) {
mD.makingDrinking();
}
//全局函数-->②父类的指针指向子类对象
void doMaking(abstractMakingDrinking* mD) {
mD->makingDrinking();
delete mD;//释放
}
int main() {
//父类指针②指向子类对象---冲咖啡
abstractMakingDrinking* md = new makingCoffee;
md->makingDrinking();
delete md;//释放
cout << "------------------" <<endl;
//全局函数-->①父类的引用指向子类对象---冲茶叶
makingTee mt;
doMaking(mt);
cout << "------------------" <<endl;
//全局函数-->②父类的指针指向子类对象
doMaking(new makingCoffee);
cout << "------------------" <<endl;
doMaking(new makingTee);
cout << "------------------" <<endl;
system("pause");
return 0;
}
4.7.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码.
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
4.7.6 多态案例三—电脑组装
1.各个零部件有父类,然后继承、重写、实现多态(父类指针指向子类对象,这里就会有各个零件的功能函数)
2.把各个零件的功能函数一起写到computer类中。
程序:
#include<iostream>
using namespace std;
//void usingCPU(abstractCPUs* cpu);
//void usingGPU(abstractGPUs* gpu);
//void usingMemory(abstractMemorys* memory);
//父类:CPU
class abstractCPUs {
public:
//CPU的计算功能
virtual void calculate() = 0;//virtual void cpuInfo() = 0;
};
//父类:显卡
class abstractGPUs {
public:
//GPU的显示功能
virtual void display() = 0;//virtual void gpuInfo() = 0;
};
//父类:内存条
class abstractMemorys {
public:
//内存条的存储功能
virtual void storage() = 0;//virtual void memoryInfo() = 0;
};
//CPU子类://Intel,AMD,IBM
class CPU1:public abstractCPUs{
public:
virtual void calculate() {
cout << "Intel牌CPU开始计算了" << endl;
}
};
class CPU2 :public abstractCPUs {
public:
virtual void calculate() {
cout << "AMD牌CPU开始计算了" << endl;
}
};
class CPU3 :public abstractCPUs {
public:
virtual void calculate() {
cout << "IBM牌CPU开始计算了" << endl;
}
};
//全局函数--->父类指针/引用指向子类对象
void usingCPU(abstractCPUs* cpu) {
cpu->calculate();//接口
delete cpu;
}
//GPU子类: NVIDIA、AMD
class GPU1 :public abstractGPUs {
public:
virtual void display() {
cout << "NVIDIA牌GPU开始显示了" << endl;
}
};
class GPU2 :public abstractGPUs {
public:
virtual void display() {
cout << "AMD牌GPU开始显示了" << endl;
}
};
//全局函数--->父类指针/引用指向子类对象
void usingGPU(abstractGPUs* gpu) {
gpu->display();//接口
delete gpu;
}
//内存条子类:ADATA USCORSAIR Kingston
class memory1 :public abstractMemorys{
public:
virtual void storage() {
cout << "ADATA牌内存条开始存储" << endl;
}
};
class memory2 :public abstractMemorys {
public:
virtual void storage() {
cout << "USCORSAIR牌内存条开始存储" << endl;
}
};
class memory3 :public abstractMemorys {
public:
virtual void storage() {
cout << "Kingston牌内存条开始存储" << endl;
}
};
//全局函数--->父类指针/引用指向子类对象
void usingMemory(abstractMemorys* memory) {
memory->storage();//接口
delete memory;
}
class Computer {
public:
void doWork(abstractCPUs* cpu, abstractGPUs* gpu, abstractMemorys* memory) {
cout << "开始组装电脑:" << endl;
usingCPU(cpu);
usingGPU(gpu);
usingMemory(memory);
cout << "组装完成!\n" << endl;
}
};
int main() {
Computer computer;
computer.doWork(new CPU1,new GPU1,new memory1);
computer.doWork(new CPU2, new GPU2, new memory2);
computer.doWork(new CPU3, new GPU1, new memory3);
system("pause");
return 0;
}
结果:
开始组装电脑:
Intel牌CPU开始计算了
NVIDIA牌GPU开始显示了
ADATA牌内存条开始存储
组装完成!
开始组装电脑:
AMD牌CPU开始计算了
AMD牌GPU开始显示了
USCORSAIR牌内存条开始存储
组装完成!
开始组装电脑:
IBM牌CPU开始计算了
NVIDIA牌GPU开始显示了
Kingston牌内存条开始存储
组装完成!
请按任意键继续. . .
5、文件操作
文件操作的头文件#include<fstream>
文件类型:
文本文件:以ASCII码的形式
存在计算机中
二进制文件:以二进制(0和1)的形式
存在计算机中
操作文件的三大类;
写操作:ofstream(output file stream)输出是写;`写出来`
读操作:ifstream(input file stream)输入是读;`进去读`
读写操作:fstream,可读可写。
5.1 文本文件
5.1.1 写文件
写文件步骤如下:
- 包含头文件
#include <fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open("文件路径",打开方式);
- 写数据
ofs << "写入的数据";
- 关闭文件
ofs.close();
其中,第三步的文件打开方式包括以下几种:
| 打开方式 | 解释 |
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除,再创建 |
| ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用|或操作符
例如:用二进制方式写文件 ios::binary | ios:: out
5.1.2 读文件
读文件步骤如下:
- 包含头文件
#include <fstream>
- 创建流对象
ifstream ifs;
- 打开文件并判断文件是否打开成功
ifs.open("文件路径",打开方式);
利用is_open函数
可以判断文件是否打开成功 - 读数据
四种读取方式
- 关闭文件
ifs.close();
第四步中的四种读取方式:
//读数据
//方式1:
//char buf[1024] = {0};
//while (ifs >> buf)//右移运算符
// cout << buf << endl;
//方式2
//char buf[1024] = { 0 };
//while (ifs.getline(buf, sizeof(buf)))
// cout << buf << endl;
//方式3
//string buf;
//while (getline(ifs, buf))
// cout << buf << endl;
//方式4(不推荐)
//char c;
//while ((c = ifs.get()) != EOF)//EOF == End of file
// cout << c;// <<endl 这种方法就不要这个回车符了
程序:
#include<iostream>
#include<fstream>//文件流
#include<string>
using namespace std;
int main() {
//写文件:①头文件②文件流对象③打开文件(路径+方式)④写数据⑤关闭文件
//创建流对象
ofstream ofs;
//打开文件:包括文件路径和打开方式
ofs.open("E:\\c++example\\0723\\Project1\\file_operating\\test.txt",ios::out);//
//写数据
ofs << "今天是周四,天气晴,现在在学习c++中的文件操作。" <<endl;
ofs << "测试完毕,over。" << endl;
//关闭文件
ofs.close();
//读文件:①头文件②文件流对象③打开文件并判断文件是否打开成功④读数据⑤关闭文件
//创建流对象
ifstream ifs;
//打开文件:包括文件路径和打开方式
ifs.open("E:\\c++example\\0723\\Project1\\file_operating\\test.txt", ios::in);
//判断文件是否打开成功
if (ifs.is_open() == 0) {
cout << "文件打开失败!!!" << endl;
return 0;
}
//读数据
//方式1:
//char buf[1024] = {0};
//while (ifs >> buf)//右移运算符
// cout << buf << endl;
//方式2
//char buf[1024] = { 0 };
//while (ifs.getline(buf, sizeof(buf)))
// cout << buf << endl;
//方式3
//string buf;
//while (getline(ifs, buf))
// cout << buf << endl;
//方式4(不推荐)
//char c;
//while ((c = ifs.get()) != EOF)//EOF == End of file
// cout << c;// <<endl 这种方法就不要这个回车符了
//关闭文件
ifs.close();
system("pause");
return 0;
}
5.2 二进制文件
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write
。函数原型 :ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
例如:
//int nNum = 20; fout.write((char*)&nNum, sizeof(int));
//string str("Hello, world"); fout.write(str.c_str(), sizeof(char) *(str.size()));
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read
。函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
例如:
//如果是整形需转成(char*)//int nNum1;fin.read((char*)&nNum1, sizeof(int));
//如果是字符串//char szBuf[256] = { 0 };fin.read(szBuf,sizeof(char) * 256);
程序(再写一遍再补充过来):
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main() {
/*文本文件 //"E:\\c++example\\0723\\Project1\\file_operating\\test2.txt"
//写out
ofstream ofs("E:\\c++example\\0723\\Project1\\file_operating\\test2.txt",ios::out);
if (!ofs.is_open()) {
cout << "打开文件错误!!!" << endl;
return 0;
}
ofs << "文件操作第二遍\nover" << endl;
ofs.close();
//读in
ifstream ifs("E:\\c++example\\0723\\Project1\\file_operating\\test2.txt", ios::in);
if (!ifs.is_open()) {
cout << "打开文件错误!!!" << endl;
return 0;
}
//读取方式3
string str;
while (getline(ifs, str))
cout << str << endl;
ifs.close();
*/
//二进制文件 //"E:\\c++example\\0723\\Project1\\file_operating\\test3.txt"
//写out
ofstream ofs("E:\\c++example\\0723\\Project1\\file_operating\\test3.txt",ios::out | ios::binary);
if (!ofs.is_open()) {
cout << "文件打开错误" << endl;
return 0;
}
//写数据
int num = 10;
string name = "abcdefg";
ofs.write((const char*)&num, sizeof(int));
ofs.write(name.c_str(),sizeof(char)*name.size());
ofs.close();
//读in
ifstream ifs("E:\\c++example\\0723\\Project1\\file_operating\\test3.txt", ios::in | ios::binary);
if (!ifs.is_open()) {
cout << "文件打开错误" << endl;
return 0;
}
//读数据
int num1;
//string str;
char buf[256] = { 0 };
ifs.read((char*)&num1, sizeof(int));
//ifs.read((char *)&str, sizeof(str));//sizeof(char) * str.size()
ifs.read(buf, sizeof(buf));//
cout << num1 << " " << buf << endl;
ifs.close();
system("pause");
return 0;
}
总结:
文本文件读写:
写:
ofstream ofs(“E:\\
c++example\\
test2.txt”,ios::out
);
if (!ofs.is_open()) {
cout << “打开文件错误!!!” << endl;
return 0;
}
//写数据
ofs << “文件操作第二遍\nover” << endl;
ofs.close();
读:
ifstream ifs(“E:\\
c++example\\
test2.txt”, ios::in
);
if (!ifs.is_open()) {
cout << “打开文件错误!!!” << endl;
return 0;
}
//读取方式3
//string str;
//while (getline(ifs, str))
// cout << str << endl;
ifs.close();
二进制文件读写:
写:
ofstream ofs(“E:\\
c++example\\
test3.txt”,ios::out | ios::binary
);
if (!ofs.is_open()) {
cout << “文件打开错误” << endl;
return 0;
}
//写数据
//int num = 10;
//string name = "abcdefg";
//ofs.write((const char*)&num, sizeof(int));
//ofs.write(name.c_str(),sizeof(char)*name.size());
ofs.close();
读:
ifstream ifs(“E:\\
c++example\\
test3.txt”, ios::in | ios::binary
);
if (!ifs.is_open()) {
cout << “文件打开错误” << endl;
return 0;
}
//读数据
int num1;
//string str;错误!!!
char buf[256] = { 0 };
ifs.read((char*)&num1, sizeof(int));
//ifs.read((char *)&str, sizeof(str));//sizeof(char) * str.size()错误!!!
ifs.read(buf, sizeof(buf));//
cout << num1 << " " << buf << endl;
ifs.close();
6 基于多态的企业职工系统资料
职工管理系统可以用来管理公司内所有员工的信息
公司中职工分为三类:普通员工、经理、老板,显示信息时,需要显示职工编号、职工姓名、职工岗位、以及职责。
普通员工职责:完成经理交给的任务
经理职责:完成老板交给的任务,并下发任务给员工
老板职责:管理公司所有事务
管理系统中需要实现的功能如下:
- 退出管理程序:退出当前管理系统
- 增加职工信息:实现
批量
添加职工功能,将信息录入到文件中,职工信息为:职工编号、姓名、部门编号
- 显示职工信息:显示公司内部所有职工的信息
- 删除离职职工:按照
编号
删除指定的职工 - 修改职工信息:按照
编号
修改职工个人信息 - 查找职工信息:按照
职工的编号或者职工的姓名
进行查找相关的人员信息 - 按照编号排序:按照
职工编号
,进行排序,排序规则由用户指定 - 清空所有文档:清空文件中记录的所有职工信息 (清空前需要再次确认,防止误删)
添加职工:
功能:
批量添加职工,并且保存到文件中。
分析:
批量添加职工时,可能会创建不同种类的职工;
如何将不同种类的职工放到一个数组中?可以将所有员工的指针维护到一个数组中,即new的每个子类对象返回的指针放在一个指针数组里;
如果想在程序中维护这个长度不定的指针数组,可以将数组创建到堆区,并利用abstractWorker **的指针维护。
(补充)指针数组 & 数组指针:
首先需要明确一个优先级顺序:()>[]>*
,所以:
(*p)[n]:根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针;
*p[n]:根据优先级,先看[],则p是一个数组,再结合 *,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组。