课程源自B站:https://www.bilibili.com/video/BV1et411b73Z
C++核心编程
1 内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
- 代码区:存放函数的二进制代码,由操作系统进行管理
- 全局区:存放全局变量、静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区的意义:
不同的区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
1.1 程序运行前
在程序编译后,形成了exe可执行程序,未执行该程序前分为两个区域。
代码区
- 存放cpu执行的机器指令(二进制)
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外的修改了他的指令
全局区
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量也存放在此
(其他常量:const修饰的就是其他常量)
该区域的数据在程序结束后由操作系统释放
名词解释:
- 局部变量:在函数体内部的变量,比如之前在笔记(一)、(二)中的所写的变量都是局部变量。
- 全局变量:在函数体int main() 之外的变量。[全局区]
- 静态变量:在普通变量之前加static,就变成了静态变量。[全局区]
- 常量(分为两种类型)
- 字符串常量:双引号修饰的。[全局区]
- const修饰的常量(又分为两种)
- const修饰的全局常量:在全局常量前加const [全局区]
- const修饰的局部常量:在局部常量前加const
1.2 程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值、局部变量等。
- 注意事项:不要返回局部变量的地址,栈区开辟的地址由编译器自动释放。
程序示例:
#include<iostream>
using namespace std;
//栈区数据注意事项 —— 不要返回局部变量的地址
//栈区的数据由编译器管理开辟和释放
int * function()
{
int a = 10; //a是一个局部变量 存放在栈区,栈区的数据在函数执行完毕后自动释放
return &a; //返回一个局部变量的地址
}
int main()
{
int * p = function();
cout << *p << endl; //第一次可以打印正确的数字,是因为编译器做了保留
cout << *p << endl; //第二次这个数据就不保留了
system("pause");
return 0;
}
程序运行结果:
总结:因为栈区存放的数据在函数执行完毕之后就会被编译器自动释放,因此不能返回局部变量的地址。(这里第一次可以输出正确数据的原因是,编译器照顾你这个不懂的傻子,但是只给你一次机会/狗头)
堆区:
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟内存
示例:
#include<iostream>
using namespace std;
int * function()
{
//利用new关键字,在堆区开辟区域 存放数据
int * p = new int(10); // 利用new关键字,指定类型,输入数据值,系统自己分配地址,然后定义一个指针接收
//这个指针本质上是局部变量,放在栈区,指向的是堆区的地址,只不过在马上死亡的时候将堆区的地址传给了主函数
return p; //返回这个指针
}
int main()
{
int * p = function();
cout << *p << endl;
cout << *p << endl;
cout << *p << endl; //可以正常打印
cout << *p << endl;
system("pause");
return 0;
}
类比上一个程序,这个可以打印,因为10这个数据存放在堆区。
1.3 new操作符
- C++中利用new操作符在堆区中开辟数据
- 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
- 语法:
new 数据类型
- 利用new创建的数据,会返回该数据对应的类型的指针
示例:
#include<iostream>
using namespace std;
//1.new的基本语法
//用new在堆区开辟数据存放地址,用delete释放
int * func()
{
int *p = new int(10);//new定义的数据类型返回的的是 该数据类型的指针
return p;
}
void test01()
{
int * p = func();
cout << *p << endl;
cout << *p << endl;
//想要释放堆区里的数据,利用delete关键字
delete p;//释放数据的地址,之后就不能再访问这块地址了
}
//2.在堆区利用new开辟数组
void test02()
{
//在堆区开辟一个整型数组
int * p = new int[10]; //开辟数组要用中括号,括号里表示数组的容量,返回的是数组的首地址,用指针接收
//给数组赋值,方法和普通数组一样
for (int i = 0; i < 10; i++)
{
p[i] = i + 1;
}
//读取数组的值,方法和普通数组一样
for (int i = 0; i < 10; i++)
{
cout << p[i] << endl;
}
}
int main()
{
test01();
cout << "test02的结果: " << endl;
test02();
system("pause");
return 0;
}
程序运行结果:
2 引用
2.1 引用的基本使用
作用:给变量起别名。
语法:数据类型 &别名 = 原名
示例:
在这里插入代码片/*********************************************************
课程名称:引用的基本使用
**********************************************************/
#include<iostream>
using namespace std;
int main()
{
//创建变量
int a = 10;
//创建引用
int &b = a;
cout << "a= " << a << endl;
cout << "b= " << b << endl;
b = 100;
cout << "a= " << a << endl;
cout << "b= " << b << endl;
system("pause");
return 0;
}
程序运行结果:
总结:可以把引用理解为给变量起小名,对原名和小名进行操作都会影响到数值。
2.2 引用的注意事项
- 引用必须初始化(现有大名,才能再起小名)
- 引用在初始化之后就不应该做出改变(小名给一个人用过之后,就不能再给别人用了)
2.3 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
示例:
/*********************************************************
课程名称:引用做函数参数
**********************************************************/
#include<iostream>
using namespace std;
void Swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int A = 10;
int B = 20;
Swap(A, B);
cout << "A= " << A << endl;
cout << "B= " << B << endl;
system("pause");
return 0;
}
程序运行结果:
总结:虽然不是地址传递,也可以实现对实参进行修改。这样可以不用指针,更加方便。
2.4 引用做函数的返回值
作用:引用是可以作为函数返回值存在的。
注意:不要返回局部变量的引用。
用法:函数调用作为左值
注:所谓的左值,就是等号左边的值,就是可以被赋值的值。
示例:
/*********************************************************
课程名称:引用做函数的返回值
**********************************************************/
#include<iostream>
using namespace std;
//1、不要返回局部变量的引用
int& test01() //在返回值类型后面加一个&,就代表返回的是引用
{
int a = 10;
return a;
}
//2.做返回值的引用是可以修改的左值
int & test02()
{
static int b = 20; //静态变量储存在全局区,所以子函数运行完不会被释放,可以返回其引用值
return b;
}
int main()
{
cout << "test01运行结果: " << endl;
int & ref = test01(); //创建一个引用来接收子函数的值
cout << "ref= " << ref << endl; //第一次正常是因为编译器做了保留
cout << "ref= " << ref << endl; //因为局部变量在栈区,子函数运行完之后变量被释放
cout << endl << "test02运行结果: " << endl;
int & ref02 = test02();
cout << "ref02= " << ref02 << endl;
cout << "ref02= " << ref02 << endl;
cout << "ref02= " << ref02 << endl;
test02() = 100; //返回类型为引用的子函数可以作为左值,可以被修改
cout << "ref02= " << ref02 << endl;
cout << "ref02= " << ref02 << endl;
system("pause");
return 0;
}
程序运行结果:
2.5 引用的本质
本质:引用的本质在C++内部实现是一个指针常量。
知识回忆:指针常量:指针的指向不能变,但是指针指向的值可以改变。
程序运行的根本步骤:
当你书写一段:
int a=10;
int& ref=a;
的时候,相当于编译器自动写了一段代码:
int * const ref=&a;
而当你对ref进行操作的时候:
ref=100;
系统识别到是引用,自动转换成了:
*ref=100;
相当于自动解引用。这一点在引用做函数参数的时候也是相同的,其实还是用指针来进行接受传入实参的地址,本质还是地址传递。
同时,联系之前2.2所学习的,引用只能作为一个人的小名,因为引用是一个指针常量,所以该指针的指向不能改变,因此只能成为一个人的小名。
下面还有一段代码,加深理解:
//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
int a = 10;
//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
cout << "a:" << a << endl;
cout << "ref:" << ref << endl;
func(a);
return 0;
}
结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了
2.6 常量引用
作用:常量引用主要来修饰形参,防止误操作
语法:void Func(const int & ref);
如果在传入的变量前加一个const,在子函数内部就不能对传入的值进行修改,换句话说:本来是地址传递,现在变成了值传递。
深入理解:上一节说到引用是一个指针常量,相当于int * const p
,再加一个const,相当于const int * const p
,指针的指向和指针指向的内容均不能修改。
示例:
//引用使用的场景,通常用来修饰形参
void showValue(const int& v)
{
//v += 10;
cout << v << endl;
}
int main() {
//int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;
//ref = 100; //加入const后不可以修改变量
cout << ref << endl;
//函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);
system("pause");
return 0;
}
3 函数高级
3.1 函数默认参数
在C++中,自定义函数的时候,可以一上来给形参一个默认的值。
语法:返回值类型 函数名 (参数=默认值){ }
- 如果自己传入数据,就用自己的数据,如果没有传入数据,就用默认值
示例:
/*********************************************************
课程名称:函数默认参数
**********************************************************/
#include<iostream>
using namespace std;
int Add(int a, int b=10, int c=20)
{
return a + b + c;
}
int main()
{
cout << Add(10, 20) << endl; //有默认参数的情况下,不一定要给该参数赋值,比如说这里的c
//有自己赋值的情况下,优先用自己的赋值而不是默认值,比如说这里的b
system("pause");
return 0;
}
程序运行结果:
注意事项:
- 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
如图,b有默认值,而右边的c和d就没有默认值,因此会爆红线警告。 - 如果函数在声明的时候给出了默认参数,则在函数实现的时候就不能再给出(重定义)默认参数了。
3.2 函数占位参数
C++的形参列表里可以有占位参数,用来做占位,调用函数时必须补齐该位置
语法:返回值类型 函数名 (数据类型){}
注意:在形参列表里,只写数据类型而不写数据名
现阶段函数占位参数存在意义不大,后面的课程中会用到该技术
Tips:占位参数也可以有默认参数,如:
void func(int = 10){}
3.3 函数重载
3.3.1 函数重载概述
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下(现阶段写的代码都在全局作用域下)
- 函数名称相同
- 函数参数类型不同,或者个数不同,或者顺序不同
注意:函数的返回值不能作为函数重载的条件
示例:
/*********************************************************
课程名称:函数重载
**********************************************************/
#include<iostream>
using namespace std;
//函数重载的三个条件
//1.在同一区域
//2.函数名称相同
//3.形参的 类型不同 或 顺序不同 或 数量不同
void func()
{
cout << "void func()" << endl;
}
void func(int a)
{
cout << "void func(int a)" << endl;
}
void func(int a,int b)
{
cout << "void func(int a,int b)" << endl;
}
void func(int a, double b)
{
cout << "void func(int a, double b)" << endl;
}
void func(double a, int b)
{
cout << "void func(double a, int b)" << endl;
}
int main()
{
func();
func(10);
func(10.10);
func(10.3, 14);
func(10, 3.14);
system("pause");
return 0;
}
程序运行结果:
由运行结果可知,在函数名相同的情况下,只要形参不一样,编译器就能区分出你到底想要调用哪个函数,如果形参一样的话,那么编译器就没有办法分辨了。
3.3.1 函数重载注意事项
- 引用作为函数重载的条件
示例:
/*********************************************************
课程名称:函数重载的注意事项
引用作为函数重载的条件
**********************************************************/
#include<iostream>
using namespace std;
//引用作为函数重载的条件
//引用值的不同也可以满足函数重载的条件
void func(int &a)
{
cout << "调用func(int &a)" << endl;
}
void func(const int &a)
{
cout << "调用func(const int &a)" << endl;
}
int main()
{
int a = 10;
func(a); //调用第一个函数
func(10); //调用第二个函数
system("pause");
return 0;
}
程序运行结果:
- 函数重载碰到函数默认参数
当我们写了上图的参数之后,用到了参数的默认值,语法上是满足函数重载的条件的,但是当调用这个函数时:
编译器就不知道具体用哪一个函数了,所以应该避免
4 类和对象
C++面向对象的三大特性为:封装、继承和多态
C++认为万事万物皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
4.1 封装
4.1.1 封装的意义
封装是C++面向对象的三大特性之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
4.1.1.1 封装的意义一
在设计类的时候,属性和行为写在一起,表现事物
语法: class 类名{ 访问权限: 属性 / 行为 };
示例一:写一个“圆”类,求圆的周长
/*********************************************************
课程名称:封装的意义
代码演示:设计一个圆类,求圆的周长
制作人:Wu_XZ
**********************************************************/
#include<iostream>
using namespace std;
#define pi 3.14159
class Circle //class+类名
{
//访问权限
public: //公共访问权限
//属性
int Cir_r;
//行为:给出圆的周长,这里用一个函数
double Cir_ZC()
{
return 2 * pi*Cir_r;
}
};
int main()
{
Circle c1; //类似结构体,把Circle看作是一个变量类型
c1.Cir_r = 10; //给半径赋值,跟结构体调用数据的语法一样
cout << "圆的周长为: " << c1.Cir_ZC() << endl; //这里注意c1.Cir_ZC()加括号
system("pause");
return 0;
}
程序运行结果:
**示例2:**设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
/*********************************************************
课程名称:封装的意义
代码演示:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;
#define pi 3.14159
class Student //class+类名
{
//访问权限
public: //公共访问权限
//属性
string name;
string StuID;
//行为:给出圆的周长,这里用一个函数
void PrintInfo()
{
cout << "学生的姓名: " << name << endl;
cout << "学生的学号: " << StuID << endl;
}
};
int main()
{
Student s1; //类似结构体,
s1.name = "Yan_EB";
s1.StuID = "201704060704";
s1.PrintInfo();
system("pause");
return 0;
}
程序运行结果:
4.1.1.2 几个专业术语
- 类中的属性和行为,统称为成员
- 属性又称:成员属性、成员变量
- 行为又称:成员函数、成员方法
4.1.1.3 用行为来给属性赋值
上述示例二中,也可在行为中定义赋值函数,对成员变量进行赋值
示例:
/*********************************************************
课程名称:封装的意义
代码演示:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;
#define pi 3.14159
class Student //class+类名
{
//访问权限
public: //公共访问权限
//属性
string name;
string StuID;
//行为:给出圆的周长,这里用一个函数
void PrintInfo()
{
cout << "学生的姓名: " << name << endl;
cout << "学生的学号: " << StuID << endl;
}
/*************创建两个赋值函数*************/
void SetName(string n)
{
name = n;
}
void SetID(string m)
{
StuID = m;
}
};
int main()
{
Student s1; //类似结构体,
/*s1.name = "Yan_EB";
s1.StuID = "201704060704";*/
s1.SetName("Alan"); //这里改用调用函数的形式来赋值
s1.SetID("201704060713");
s1.PrintInfo();
system("pause");
return 0;
}
程序运行结果:
4.1.1.4 封装的意义二
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
名称 | 关键字 | 说明 | 备注 |
---|---|---|---|
公共权限 | public | 成员在类内可以访问,类外也可以访问 | |
保护权限 | protected | 成员在类内可以访问,类外不可以访问 | 儿子可以访问父亲的保护内容 |
私有权限 | private | 成员在类内可以访问,类外不可以访问 | 儿子不可以访问父亲的隐私内容 |
4.1.2 struct和class的区别
在C++中struct和class唯一的区别就是默认的访问权限不同
- struct默认权限为公共
- class默认权限为私有
示例:
由图不难看出,在不指定权限的情况下,由class定义的类是不能访问的。
4.1.3 成员属性设为私有
优点1:将所有成员属性设为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
示例:
/*********************************************************
课程名称:成员属性设置为私有
代码演示:1.好处一:可以自己控制读写权限
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;
class Person
{
private:
string Name; //想要:可读可写
int Age; //想要:只读不能写
string Lover; //想要:只写不能读
//这些属性都是私有属性,不可以被外部访问
//为了实现上面的需求,我们可以操作子函数放在public中
public:
//读写姓名
void setName(string name)
{
Name = name;
}
string getName()
{
return Name;
}
//只读年龄
int getAge()
{
Age = 0; //给年龄一个默认值
return Age;
}
//只写情人
void serLover(string Lname)
{
Lover = Lname;
}
};
int main()
{
Person p;
//写读姓名
p.setName("张三");
cout << "姓名: " << p.getName() << endl;
//读年龄
cout << "年龄: " << p.getAge() << endl;
//写情人
p.serLover("苍井");
system("pause");
return 0;
}
程序运行结果:
下面演示优点2,要求是:可以设置年龄,但是年龄必须要在0~150岁才可视为有效。
示例:
/*********************************************************
课程名称:成员属性设置为私有
代码演示:1.好处二:可以控制数据的有效性
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;
class Person
{
private:
string Name; //想要:可读可写
int Age; //想要:可以读,可以写,但是年龄在0-150岁才视为有效
string Lover; //想要:只写不能读
//这些属性都是私有属性,不可以被外部访问
//为了实现上面的需求,我们可以操作子函数放在public中
public:
//读写姓名
void setName(string name)
{
Name = name;
}
string getName()
{
return Name;
}
//读年龄,写年龄需要有效
void setAge(int age)
{
if (age <= 0 || age > 150)
{
cout << "你可真是个小精灵鬼儿!重输!" << endl;
return;
}
Age = age;
}
int getAge()
{
return Age;
}
//只写情人
void serLover(string Lname)
{
Lover = Lname;
}
};
int main()
{
Person p;
//写读姓名
p.setName("张三");
cout << "姓名: " << p.getName() << endl;
//写
p.setAge(1000);
//写情人
p.serLover("苍井");
system("pause");
return 0;
}
程序运行结果:
4.1.4 封装案例
4.1.4.1 练习案例1:设计立方体类
要求:
设计立方体类(Cube)
求出立方体的面积和体积
分别用全局函数和成员函数判断两个立方体是否相等。
示例:
/*********************************************************
课程名称:封装练习题
代码演示:设计立方体类
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;
class Cube
{
private:
int Len;
int Wid;
int Hei;
public:
//定义长宽高
void setLen(int l)
{
Len = l;
}
void setWid(int w)
{
Wid= w;
}
void setHei(int h)
{
Hei = h;
}
//得到长宽高
int getLen()
{
return Len;
}
int getWid()
{
return Wid;
}
int getHei()
{
return Hei;
}
//计算面积
int calS()
{
return (Wid*Len + Wid * Hei + Len * Hei) * 2;
}
//计算体积
int calV()
{
return Len * Wid * Hei;
}
/****成员函数判断两个立方体是否相等****/
bool isSame(Cube c)
{
if (Len == c.Len&&Wid == c.Wid&&Hei == c.Hei)
{
return true;
}
return false;
}
};
/****全局函数判断两个立方体是否相等****/
bool isSame_Whole(Cube &c1, Cube &c2)
{
if (c1.getHei() == c2.getHei()&&c1.getLen() == c2.getLen()&&c1.getWid() == c2.getWid())
{
return true;
}
return false;
}
int main()
{
Cube c1;
c1.setLen(10);
c1.setHei(10);
c1.setWid(10);
cout << "立方体的面积为:" << c1.calS() << endl;
cout << "立方体的体积为:" << c1.calV() << endl;
Cube c2;
c2.setLen(10);
c2.setHei(10);
c2.setWid(10);
bool ref = c1.isSame(c2);
bool ref2 = isSame_Whole(c1, c2);
if (ref)
{
cout << "成员函数:两个立方体相同" << endl;
}
if (ref2)
{
cout << "全局函数:两个立方体相同" << endl;
}
system("pause");
return 0;
}
程序运行结果:
4.1.4.2 练习案例2:点和圆的关系
要求:
设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。
示例:
/*********************************************************
课程名称:封装练习题
代码演示:设计点类和圆类 并求出点和圆之间的关系
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;
class Point
{
private:
int X;
int Y;
public:
//设置X
void setX(int x)
{
X = x;
}
//获取X
int getX()
{
return X;
}
//设置Y
void setY(int y)
{
Y= y;
}
//获取Y
int getY()
{
return Y;
}
};
class Circle
{
private:
int R;
Point Center;
public:
//设置半径
void setR(int r)
{
R = r;
}
//获取半径
int getR()
{
return R;
}
//设置圆心
void setCenter(Point cen)
{
Center = cen;
}
//获取圆心
Point getCenter()
{
return Center;
}
};
//判断点和圆关系的全局函数
void isInCircle(Circle &c, Point &d)
{
//点到圆心的距离的平方
int distance =
(c.getCenter().getX() - d.getX())*(c.getCenter().getX() - d.getX()) +
(c.getCenter().getY() - d.getY())*(c.getCenter().getY() - d.getY());
//圆的半径的平方
int rDistance = c.getR()*c.getR();
//判断
if (distance == rDistance)
{
cout << "点在圆上" << endl;
}
else if (distance > rDistance)
{
cout << "点在圆外" << endl;
}
else if (distance < rDistance)
{
cout << "点在圆内" << endl;
}
}
int main()
{
Point p1;
Point p2;
Point p3;
p1.setX(10);
p1.setY(11);
p2.setX(10);
p2.setY(10);
p3.setX(10);
p3.setY(9);
Point cen;
cen.setX(10);
cen.setY(0);
Circle c;
c.setR(10);
c.setCenter(cen);
isInCircle(c, p1);
isInCircle(c, p2);
isInCircle(c, p3);
system("pause");
return 0;
}
程序运行结果:
4.1.5 类的分文件编写
以上题为例,分别对Circle和Point两个类分别变成一个头文件,具体操作方法如下:
-
步骤一:
在“外部依赖项”中新建名为Circle.h
的头文件。
-
步骤二:
在该头文件中输入老套路
+类Circle的成员的声明
,之后每个声明后需要加补全;
注意:这里Point下面飘红是因为还没有写Point的声明,而且前面要补全头文件。 -
步骤三:
在“源文件”中添加Circle.cpp
用于写函数的实现。
-
步骤四:
在Circle.cpp中首先写#include“Circle.h”
,然后只保留函数的实现,同时,在setR()
等函数前加Circle::
告诉系统这个函数是在Circle作用域下的成员函数。
-
步骤五:
同样的方法设置Point类的份文件。 -
步骤六:
4.2 对象的初始化和清理
- 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
- C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数
- 构造函数:做初始化的函数。主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:做清理的函数。主要作用在于对象销毁前系统自动调用,执行一些清理工作。
注意:
- 这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
- 如果我们不提供构造和析构,编译器会提供析构和构造函数。
- 此时编译器提供的构造函数和析构函数是空实现。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
示例:
/*********************************************************
课程名称:构造函数和析构函数
代码演示:……
制作人:Wu_XZ
**********************************************************/
#include<iostream>
using namespace std;
class Person //先建立一个类
{
public: //这里是public才能保证从外部可以调用到
//一、构造函数
//1. 构造函数,没有返回值也不写void
//2. 函数名称与类名相同
//3. 构造函数可以有参数,因此可以发生重载
//4. 程序在调用对象时候会自动调用构造,无须手动调用, 而且只会调用一次
Person() //这里先不写参数
{
cout << "Person 构造函数的调用 " << endl;
}
//二、析构函数
//1. 析构函数,没有返回值也不写void
//2. 函数名称与类名相同, 在名称前加上符号 ~
//3. 析构函数不可以有参数,因此不可以发生重载
//4. 程序在对象销毁前会自动调用析构,无须手动调用, 而且只会调用一次
~Person()
{
cout << "Person 析构函数的调用" << endl;
}
};
void test01()
{
Person p;
}
int main()
{
test01();
system("pause");
return 0;
}
程序运行结果:
程序分析:
1.注意在这个程序中,只是在函数test01()
中定义了一个Person
类型的变量,并没有进行类似p.Person()
调用函数的操作,但是系统已经自己调用了。
2.同时也可以看到析构函数也被执行了,这是因为我们写的子函数是在栈区,子函数执行完毕之后就会被释放,所以在子函数被释放之前析构函数就会被执行。
3.如果将定义语句Person p;
放在主函数内,则程序运行之后不会出现析构函数中的语句,而在按任意键退出程序时,能看到析构函数中的语句被执行,一闪而过。
4.2.2 构造函数的分类和调用
两种分类方式:
- 按照参数分类::
- 有参构造
- 无参构造(默认构造:编译器自动提供的就是这种类型的)
- 按照类型分类:
- 普通构造
- 拷贝构造
形式:Person(const Person &p)
作用:将一个已有的实例的属性作为这个类的初始化条件
注意:必须用上面的这种形式,用引用的形式传参
三种调用方式:
-
括号法
- 默认构造函数调用
只需要创建一个该类的实例就可以调用(这里用Person类来演示)
Person p1;
- 有参构造函数调用
在创建的实例名字后面加上相应的参数
Person p2(10);
- 拷贝构造函数的调用
在创建的实例后面加上要拷贝的实例名(这里要创建一个实例p3,p3拷贝p2的属性)
Person p3(p2);
- 注意:用括号法调用构造函数的时候,后面不能加
()
,比如说像下面这样:
[错误]Person p1();
这样编译器会认为这是一个函数的声明
- 默认构造函数调用
-
显示法
- 显示法调用默认构造函数
Person p1;
- 显示法调用有参构造函数
Person p2=Person(10)
; - 显示法调用拷贝构造函数
Person p3=Person(p2);
- 注意
一、如果只有Person(10)
这样的,也可以单独成行,这里创建的是一个匿名对象,当当前执行结束后,系统会立即回收匿名对象
二、不要用拷贝构造函数初始化匿名对象,即Person(p2)
不能单独成行,因为系统默认Person(p2)=Person p2
,相当于重定义了p2
- 显示法调用默认构造函数
-
隐式转换法
- 调用有参构造函数
Person p2=10;
相当于Person p2=Person(10)
; - 调用拷贝构造函数
Person p3= p2;
- 调用有参构造函数
4.2.3 拷贝构造函数调用时机
C++中拷贝构造函数调用时机有三种情况
-
使用一个已经创建完毕的对向来初始化新对象
-
值传递的方式给函数参数传值
-
以值方式返回局部对象
Person func() {Person p1; return p1}
这里这个函数func()返回的并不是函数里的p1,而实新建了一个对象,并且用拷贝构造函数把新建的对向初始化了。
4.2.4 构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
4.2.5 深拷贝与浅拷贝
面试经典问题
- 浅拷贝:简单的赋值拷贝操作(编译器默认提供的就是这种)
- 深拷贝:在堆区重新申请空间,进行拷贝工作
浅拷贝的问题:
总结1:浅拷贝带来的问题就是,堆区的内存重复释放
总结2:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
为了解决上面的问题,需要自己写一个深拷贝构造函数
额外补充:析构函数的作用
- 如果在类中开辟了堆区数据,则管理员应该在数据销毁之前释放堆区的内存,这个操作就在析构函数中。
- 下面的例子中,m_Height为指向堆区的指针,析构函数写法如下:
4.2.6 初始化列表
作用:
给类中的属性进行初始化操作
语法:构造函数() : 属性1(值1),属性2(值2),属性3(值3).....{}
- 传统方式初始化
传统方式初始化是写一个有参构造函数,在函数体内部进行初始化操作
class Person
{
public:
//传统方式初始化
Person(int a, int b, int c)
{
m_A = a;
m_B = b;
m_C = c;
}
private:
int m_A;
int m_B;
int m_C;
};
- 初始化列表方式进行初始化
class Person
{
public:
//初始化列表方式初始化
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
{
//这里面什么都不需要写了
}
private:
int m_A;
int m_B;
int m_C;
};
4.2.7 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
这个问题可以参见4.1.4.2中点和圆的关系,在圆
类中,定义圆心也用到了点
类
提出问题:创建圆时,点
与圆
的构造与析构函数是什么样的顺序
结论:现有手机,再有人,析构时按照先入后出的原则。
4.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static
,称为静态成员
静态成员分为:
- 静态成员变量
-
所有对象共享同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化
- 静态成员变量也是有访问权限的
-
注意:静态成员变量不属于某个对象,所有对象都共享一份数据,因此静态成员变量有两种访问方式:
- 通过对象进行访问
- 通过类名进行访问
cout<<Person::m_A<<endl;
- 静态成员函数
-
所有对象共享同一个函数
-
静态成员函数只能访问静态成员变量
-
访问方式也是两种:通过对象与通过类名访问
- 静态成员函数也是有访问权限的
-
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开储存
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
- 空对象所占的内存空间为1
C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置,每个空对象也应该有一个独一无二的位置 - 非静态成员变量属于对象上
- 静态成员变量不属于对象上
- 静态/非静态成员函数均不属于对象上
4.3.2 this指针概念
通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
提出问题:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
this指针的特征
- this指针是隐含每一个非静态成员函数内的一种指针
- this指针不需要定义,直接使用即可
this指针的用途:
-
当形参和成员变量同名时,可用this指针来区分(解决名称冲突)
-
在类的非静态成员函数中返回对象本身,可使用return *this(返回对象本身用
*this
)
上面用引用的形式返回的是p2本身,如果不用引用则返回的是用拷贝构造函数构建的一个新对象,如p2’
4.3.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
空指针可以调用成员函数,但是成员函数内部不能涉及成员属性,因为此时是空指针,成员属性都没定义,自然也不能访问
不明白可以回去看视频 P116
https://www.bilibili.com/video/BV1et411b73Z?p=116
4.3.4 const修饰成员函数
常函数:
-
成员函数后加const后我们称为这个函数为常函数
-
常函数内不可以修改成员属性(在成员函数后面加const,修饰的是this指针,让指针指向的值也不能改变,解释见下面)
这里的原因跟this指针有关:- this指针的本质是指针常量,指针的指向不能改变
- 相当于是
Person * const this
- 在函数上加了const之后,相当于变成了
const Person * const this
指针指向的值也不能改变了
-
成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
-
声明对象前加const称该对象为常对象
-
常对象只能调用常函数
原因:常对象是不允许对成员属性进行修改的,普通成员函数可以对成员属性进行修改,如果常对象能调用普通成员函数的话,岂不是让你曲线救国,把成员属性给改了?
4.4 友元
对于类来说,public是所有函数或其他类均可访问,private只能自己访问,但是,在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
示例:
/***************************************
学习内容:4.3 类和对象-友元
***************************************/
#include <iostream>
#include <string>
using namespace std;
//声明一个建筑物类
class Building
{
public:
Building()
{
m_LivingRoom_ = "客厅";
m_BedRoom_ = "卧室";
}
public:
string m_LivingRoom_; //客厅
private:
string m_BedRoom_; //卧室
};
//全局函数
void GoodGay(Building &building)
{
cout << "全局函数好基友,正在访问:" << building.m_LivingRoom_;
cout << "全局函数好基友,正在访问:" << building.m_BedRoom_; //报错!
}
//测试函数
void test01()
{
Building building;
GoodGay(building);
}
int main()
{
test01();
return 0;
}
分析:因为Building类里面的成员变量m_BedRood,是私有成员,所以全局函数GoodGay()不能访问该变量,编译器也会出现相应报错
修改+语法示例
/***************************************
学习内容:4.3 类和对象-友元
***************************************/
#include <iostream>
#include <string>
using namespace std;
//声明一个建筑物类
class Building
{
//将全局函数声明前加一个friend,来声明友元
friend void GoodGay(Building &building);
public:
Building()
{
m_LivingRoom_ = "客厅";
m_BedRoom_ = "卧室";
}
public:
string m_LivingRoom_; //客厅
private:
string m_BedRoom_; //卧室
};
//全局函数
void GoodGay(Building &building)
{
cout << "全局函数好基友,正在访问:" << building.m_LivingRoom_;
cout << "全局函数好基友,正在访问:" << building.m_BedRoom_;
}
//测试函数
void test01()
{
Building building;
GoodGay(building);
}
int main()
{
test01();
return 0;
}
4.4.2 类做友元
跟上面的例子大差不差,如果我们此时有一个类class GoodGay
,这个类可以访问Building
类中的私有属性,则在进行Building
的声明时,写法如下:
class Building
{
//再类前面加一个friend,来声明友元
friend class GoodGay;
···········
}
4.4.3 成员函数做友元
跟上面的例子大差不差,如果我们此时有一个类class GoodGay
,这个类里面的一个成员函数visit()
可以访问Building
类中的私有属性,则在进行Building
的声明时,写法如下:
class Building
{
//将其他类的成员函数声明前加一个friend,来声明友元,注意其他类的成员函数需要加作用域
friend void GoodGay::visit();
···········
}
4.5 运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
4.5.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算
对于内置数据类型,编译器知道如何进行运算,如:
int a = 10;
int b = 10;
int c = a + b;
对于自定义数据类型,此时编译器就不知道该如何处理了,如:
class Person
{
public:
int m_A_;
int m_B_;
};
Person p1;
p1.m_A_=10;
p2.m_B_=10;
Person p2;
p2.m_A_=10;
p2.m_B_=10;
Person p3 = p1 + p2; //编译器不懂加法啥意思
上述问题,可以通过自己在类内写一个成员函数实现,如:
class Person
{
public:
Person PersonAddPerson(Person &p)
{
Person temp;
temp.m_A_ = this->m_A_ + p.m_A_;
temp.m_B_ = this->m_B_ + p.m_B_;
return temp;
}
public:
int m_A_;
int m_B_;
};
上面是自己起了一个名字,不同的人可能会有不同的函数名,C++直接做了一个简化,所有的人都可以取一个统一的名字:operator+
,只需要讲上面的自定义函数名改成operator+
,就实现了运算符的重载。
-
通过成员函数重载+号
class Person { public: 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; } public: int m_A_; int m_B_; };
此时,调用该成员函数时,一般用法:
Person p3 = p1.operator+(p2);
简化方法:
Person p3 = p1 + p2;
-
通过全局函数重载+号
Person operator+(Person &p1, Person &p2) { Person temp; temp.m_A_ = p1.m_A_ + p2.m_A_; temp.m_B_ = p1.m_B_ + p2.m_B_; return temp; }
此时,调用该全局函数时,一般用法:
Person p3 = operator+(p1,p2);
简化方法:
Person p3 = p1 + p2;
-
运算符重载也可以发生函数重载
以全局函数为例,在上面的基础上再添加:Person operator+(Person &p1, int num) { Person temp; temp.m_A_ = p1.m_A_ + num; temp.m_B_ = p1.m_B_ + num; return temp; }
此时,调用该全局函数时,一般用法:
Person p4 = operator+(p1,100);
简化方法:
Person p4 = p1 + 100;
4.5.2 左移运算符重载
作用:可以输出自定义数据类型,如想要直接cout出Person类变量
注意:重载左移运算符只能通过全局函数实现
基本思想:
在正常输出的时候,一般的语句为:cout << a;
这里可以看作是把变量
cout
和变量a
用运算符<<
连接了起来,可以参照a + b
,就是运算符把两个变量连接了起来,通过查cout
的定义,可以发现cout
也是一个变量,是ostream
变量,且ostream
对象只能有一个,所以需要用引用。要实现直接输出Person类变量,全局函数可以写作:void operator<<(ostream &cout, Person p1){}
同时注意到,在使用cout时,通常会一次输出很多量,如:
cout << a << b << endl;;
这是基于一种链式编程思想,每次第一次
cout
和a
经过一次<<
运算后,返回的还是一个ostream
变量,所以这个变量继续和b
进行<<
运算(即<<
运算符左边必须是ostream
类变量,右边是要输出的变量)
所以上面的重载函数也要返回的是ostream
类变量,又因为所有代码中的ostream
变量只能有一个对象,所以返回的时候也要用引用,写成:ostream& operator<<(ostream &cout, Person p1){}
示例:
/***************************************
学习内容:4.4 类和对象-运算符重载
***************************************/
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person(int m_A, int m_B) : m_A_(m_A), m_B_(m_B)
{}
public:
int m_A_;
int m_B_;
};
ostream& operator<<(ostream &cout, Person &p1)
{
cout << "a: " << p1.m_A_ << " b: " << p1.m_B_;
}
void test01()
{
Person p1(10,10);
cout << p1 << "hello world" << endl;
}
int main()
{
test01();
return 0;
}
结果
4.5.3 递增运算符重载(待补充)
【关于运算符重载的内容后续再补充,待补充……】
4.6 继承
继承是面向对象三大特征之一
有些类与类之间存在特殊的关系,如下面这张图
如上图所示,在这些类里,下级别的类除了有上级别类的共性,也有自己的特点。
用继承的好处:减少重复代码
4.6.1 继承的基本语法
语法:class [子类] : [继承方式] [父类] {子类特有的属性};
名词拓展:
- 子类,又叫派生类
- 父类,又叫基类
4.6.2 继承的方式
继承方式一共有三种:
- 公共继承 public
- 保护继承 protected
- 私有继承 private
如上图所示:
- 父类中的
private
,不管子类采用那种继承方式,都无法得到 - 采用公共继承 public:父类中的
public
继承到子类中还是public
,父类中的protected
继承到子类中还是protected
。 - 采用保护继承 protected:父类中的
public
和protected
继承到子类中都变成protected
。 - 采用私有继承 private:父类中的
public
和protected
继承到子类中都变成private
。
4.6.3 继承中的对象模型
提出问题:从父类继承过来的成员,哪些属于子类对象中?
解答:父类中所有非静态成员都会被子类继承下去
注意:父类中私有成员属性,是被编译器给隐藏了,因此访问不到,但是确实被继承下去了
这里还介绍了一个查看类下所有成员属性的方法,具体见原视频(Visio Studio自带工具)
4.6.4 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
调用的顺序如上图所示,顺序为:
- 父类构造
- 子类构造
- 子类析构
- 父类析构
4.6.5 继承同名成员处理方式
提出问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或者父类中同名的数据呢?
解答:
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加父类的作用域
- 同名的成员属性:
[对象名].[父类名]::[属性名]
- 同名的成员函数:
[对象名].[父类名]::[函数名]
- 同名的成员属性:
例1:现在有父类Base和子类Son,二者中都有一个属性为m_A,创建一个子类对象
Son s
,放问子类对象的m_A时,直接s.m_A
,访问父类父类中的m_A时,需要s.Base::m_A
例2:现在有父类Base和子类Son,二者中都有一个函数为func(),创建一个子类对象
Son s
,放问子类对象的func()时,直接s.func()
,访问父类父类中的func()时,需要s.Base::func()
注意:当子类出现了和父类中相同名字的函数时,即使父类中发生函数重载,也不能直接调用,原因是:出现了同名的情况,子类的同名成员会隐藏掉父类中所有的同名成员函数。
例:
class Base
{
public:
void func()
{
.....
}
void func(int a)
{
.....
}
};
class Son : Base
{
public:
void func()
{
.....
}
};
此时如果想调用父类中的void func(int a)
,则不能直接调用,还得加父类的作用域。
4.6.6 继承同名静态成员处理方式
静态成员和非静态成员出现同名。处理方式一致
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
4.6.7 多继承语法
C++中允许一个类继承多个类,即允许一个儿子认多个爹
语法:class 子类 :继承方式 父亲1,继承方式,父亲2,.....
多继承可能会引发父类中有同名成员出现,需要加作用域加以区分
注意:C++实际开发中不建议使用多继承
4.6.8 菱形继承
-
菱形继承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承这两个派生类
- 这种继承被成为菱形继承,或者钻石继承,如下图:
-
菱形继承问题:
- 羊继承了动物的数据(比图兽龄),驼也继承了动物的数据,当羊驼使用数据时,就会产生二义性——>可以加以作用域区分
- 羊驼继承自动物的数据继承了两份,但是这份数据只需要一份即可
对于第二个问题:利用虚继承,可以解决菱形继承的问题:在继承之前,加上关键字virtual
语法:
class Sheep : virtual public Animal
{};
class Tuo : virtual public Animal
{};
上面的Animal类称为虚基类,利用虚继承后,羊和驼中的年龄属性就只有一个了
拓展了一下虚继承的底层原理,可以去原视频中看相关代码。
示例:
#include<iostream>
using namespace std;
class Animal
{
public:
int age_;
};
class Sheep : virtual public Animal //虚继承
{
};
class Tuo : virtual public Animal //虚继承
{
};
class Sheeptuo : public Sheep, public Tuo
{
};
void test01()
{
Sheeptuo st;
st.age_ = 18;
cout << "st.age_ = " << st.age_ << endl;
}
int main()
{
test01();
return 0;
}
4.7 多态
4.7.1 多态的基本概念
多态是C++面向对象三大特性之一
- 多态分为两类:
- 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
- 静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
示例1:地址早绑定,在编译阶段确定函数地址
#include<iostream>
using namespace std;
class Animal
{
public:
void Speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void Speak()
{
cout << "小猫在说话" << endl;
}
};
void DoSpeak(Animal &animal)
{
animal.Speak();
}
void test01()
{
Cat cat;
DoSpeak(cat);
}
int main()
{
test01();
return 0;
}
结果:
分析:
在编译时,由于
DoSpeak(Animal &animal)
函数传入的是Animal类的变量,所以使用类方法Speak()
时用的时Animal类里面的方法,函数地址被早绑定
示例2:地址晚绑定,运行阶段确定函数地址
将上述函数中的Animal类的Speak函数定义前面加上virtual
,使之变成一个虚函数,如下:
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void Speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void Speak()
{
cout << "小猫在说话" << endl;
}
};
void DoSpeak(Animal &animal)
{
animal.Speak();
}
void test01()
{
Cat cat;
DoSpeak(cat);
}
int main()
{
test01();
return 0;
}
结果:
分析:
当Animal类中的Speak()函数变成虚函数,在
DoSpeak(Animal &animal)
函数进行编译时,便不会绑定函数的地址,回根据传入的变量的类型确定调用哪一个Speak()
函数
总结1:动态多态满足条件:
1、有继承关系
2、子类重写父类的虚函数(重写:与重载不同,重写是指函数返回值类型、函数名、参数列表与被重写的函数完全相同)
总结2:动态多态使用:
父类的指针/引用指向子类的对象(如DoSpeak(Animal &animal)
中是父类的引用,传值时用的是子类的对象Cat)
补充说明:子类重写父类的虚函数时,子类的函数前面可以加
virtual
,也可以不加
4.7.2 多态的原理剖析
运行下面一段代码:
#include<iostream>
using namespace std;
class Animal
{
public:
void Speak()
{
cout << "动物在说话" << endl;
}
};
void test01()
{
cout << "the size of Animal = " << sizeof(Animal) << endl;
}
int main()
{
test01();
return 0;
}
输出结果为:
分析:对于上述代码中的Animal类来说,他没有属性(非静态成员函数不属于类),所以大小为1(空类的大小为1个字节)。
将类中的成员函数变成虚函数,执行以下代码:
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void Speak()
{
cout << "动物在说话" << endl;
}
};
void test01()
{
cout << "the size of Animal = " << sizeof(Animal) << endl;
}
int main()
{
test01();
return 0;
}
结果:
分析:这里变成8个字节是因为Animal类中存储了一个指针(64位中指针为8字节,32位中指针为4字节)。
具体来说,该指针为虚函数指针(virtual function pointer, vfptr)
多态分析:
当使用虚函数时,类中存储该虚函数指针:
当创建Cat类继承Animal类时,上述的虚函数指针被Cat一并继承
当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址。
当父类的指针或者引用指向子类对象的时候,发生多态
4.7.3 多态案例1——计算器类
案例描述:
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
- 多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的拓展以及维护
普通方法实现计算器功能:
#include<iostream>
#include<string>
using namespace std;
//实现计算器功能,普通方法
class Calculator
{
public:
int operation(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;
}
}
public:
int m_Num1;
int m_Num2;
};
void test01()
{
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.operation("+") << endl;
cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.operation("-") << endl;
cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.operation("*") << endl;
}
int main()
{
test01();
return 0;
}
结果:
分析:
上述代码,如果以后需要添加别的操作,比如除法、乘方之类,就需要在源码的基础上进行修改
但是在开发中,提倡开闭原则,即对拓展进行开放,对修改进行关闭
如果用多态实现以上功能:
#include<iostream>
#include<string>
using namespace std;
//实现计算器功能,多态方法
class CalculatorBase
{
public:
virtual int operation() //只写一个虚函数,没有内容
{
}
public:
int m_Num1;
int m_Num2;
};
//加法类
class AddCaltor : public CalculatorBase
{
int operation() //子类里面重写虚函数
{
return m_Num1 + m_Num2;
}
};
//减法类
class SubCaltor : public CalculatorBase
{
int operation() //子类里面重写虚函数
{
return m_Num1 - m_Num2;
}
};
//乘法类
class MulCaltor : public CalculatorBase
{
int operation() //子类里面重写虚函数
{
return m_Num1 * m_Num2;
}
};
void test01()
{
//使用多态的条件
//父类的指针或者引用指向子类的对象(这里用父类的指针)
CalculatorBase * c = new AddCaltor; //让父类的指针指向子类的对象
c->m_Num1 = 10;
c->m_Num2 = 10;
cout << c->m_Num1 << "+" << c->m_Num2 << "=" << c->operation() << endl;
delete c; //new的变量存放在堆区,用完要及时释放
c = new SubCaltor; //delete的时候只清楚了变量值,没有删除变量本身
c->m_Num1 = 10;
c->m_Num2 = 10;
cout << c->m_Num1 << "-" << c->m_Num2 << "=" << c->operation() << endl;
delete c; //new的变量存放在堆区,用完要及时释放
c = new MulCaltor; //delete的时候只清楚了变量值,没有删除变量本身
c->m_Num1 = 10;
c->m_Num2 = 10;
cout << c->m_Num1 << "*" << c->m_Num2 << "=" << c->operation() << endl;
delete c; //new的变量存放在堆区,用完要及时释放
}
int main()
{
test01();
return 0;
}
结果:
分析:
上述代码虽然代码量增大了,但是有了开头提到的优点,结构更清晰,以后想要拓展的时候也不用管源码了,直接新建子类重写虚函数就行,多态的有点在这种简单的功能上体现不出来,但是当代码量很大时就能体现出来。
补充:
对于上述代码,想要直接新建一个子类进行运算,虚函数不可访问
4.7.4 纯虚函数和抽象类
在多态中,通常父类中的纯虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类也被成为抽象类
抽象类特点:
1.无法实例化对象
2.子类必须重写抽象类中的函数,否则也属于抽象类
4.7.5 多态案例2——制作饮品
案例描述:
制作饮品的大致流程为:煮水-冲泡-倒入杯中-加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
示例:
#include<iostream>
#include<string>
using namespace std;
//实现制作饮品功能
class DrinkBase //制作饮品的抽象类
{
public:
//煮水
virtual void Boil() = 0; //纯虚函数
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void Pour() = 0;
//加辅料
virtual void Add() = 0;
void MakingDrink()
{
Boil();
Brew();
Pour();
Add();
}
};
//制作茶叶
class Tea : public DrinkBase
{
public:
//煮水
void Boil() //重写虚函数
{
cout << "煮山泉水" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
//倒入杯中
virtual void Pour()
{
cout << "倒入杯中" << endl;
}
//加辅料
virtual void Add()
{
cout << "加入枸杞" << endl;
}
};
//制作咖啡
class Coffee : public DrinkBase
{
public:
//煮水
void Boil() //重写虚函数
{
cout << "煮矿泉水" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡咖啡" << endl;
}
//倒入杯中
virtual void Pour()
{
cout << "倒入杯中" << endl;
}
//加辅料
virtual void Add()
{
cout << "加入糖和牛奶" << endl;
}
};
void doWork(DrinkBase * d)
{
d->MakingDrink();
delete d; //注意只要new了一个变量就要delete掉
}
void test01()
{
doWork(new Tea); //第一种方法,用new的方法
cout << "-------------------" << endl;
//第二种方法
Coffee c;
DrinkBase &cc = c;
cc.MakingDrink();
}
int main()
{
test01();
return 0;
}
结果:
4.7.6 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或纯虚析构
- 虚析构和纯析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
- 虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
示例:以之前的”小猫会说话“为例:
#include<iostream>
#include<string>
using namespace std;
//实现制作饮品功能
class Animal //制作饮品的抽象类
{
public:
Animal()
{
cout << "父类构造函数调用" << endl;
}
~Animal()
{
cout << "父类析构函数调用" << endl;
}
virtual void Speak() = 0;
};
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "子类构造函数调用" << endl;
m_Name = new string(name); //子类在堆区开辟变量
}
~Cat()
{
cout << "子类析构函数调用" << endl;
if (m_Name != NULL) //如果不为空指针,就需要delete
{
delete m_Name;
m_Name = NULL;
}
}
void Speak()
{
cout << *m_Name << "小猫说:喵喵喵" << endl;
}
public:
string * m_Name;
};
void test01()
{
Animal * abc = new Cat("Tom");
abc->Speak();
delete abc;
}
int main()
{
test01();
return 0;
}
结果:
分析:
以上代码在子类
Cat
中在堆区开辟了一个变量,并用指针m_Name
指向这个变量,并且在Cat的析构函数中对这个开辟的数据进行了delete
操作,但是,当执行程序时,由于是父类指针指向子类对象,所以并没有调用子类数据的析构函数。就会造成内存溢出
解决办法:
-
虚析构:在父类的析构函数前面加关键词
virtual
,当调用父类析构函数时,就会先调用子类的析构函数。
利用虚析构就解决了父类指针释放子类对象是不干净的问题 -
纯虚析构
语法:virtual ~类名() = 0;
同时在类外也要进行析构函数的实现(这一点跟虚函数不一样),实现方式类名::~类名(){}
示例:(上面代码改进)#include<iostream> #include<string> using namespace std; class Animal { public: Animal() { cout << "父类构造函数调用" << endl; } // virtual ~Animal() //虚析构 // { // cout << "父类析构函数调用" << endl; // } virtual ~Animal() = 0; //纯虚析构声明 virtual void Speak() = 0; }; Animal::~Animal() //纯虚析构实现 { cout << "父类析构函数调用" << endl; } class Cat : public Animal { public: Cat(string name) { cout << "子类构造函数调用" << endl; m_Name = new string(name); //子类在堆区开辟变量 } ~Cat() { cout << "子类析构函数调用" << endl; if (m_Name != NULL) //如果不为空指针,就需要delete { delete m_Name; m_Name = NULL; } } void Speak() { cout << *m_Name << "小猫说:喵喵喵" << endl; } public: string * m_Name; }; void test01() { Animal * abc = new Cat("Tom"); abc->Speak(); delete abc; } int main() { test01(); return 0; }
结果同上
纯虚析构总结:- 与纯虚函数不同,纯虚析构既要有声明也要有实现(类外实现加作用域)
- 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
4.7.7 多态案例3——电脑组装
案例描述:
电脑主要组成部件为CU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作
代码示例:
#include<iostream>
#include<string>
using namespace std;
/*-----------------创建抽象类-----------------*/
//创建CPU抽象类
class CPU
{
public:
//虚函数
virtual void Calculate() = 0;
};
//创建GPU抽象类
class GPU
{
public:
//虚函数
virtual void Display() = 0;
};
//创建内存条抽象类
class Memory
{
public:
//虚函数
virtual void Storage() = 0;
};
/*-----------------创建Computer类-----------------*/
class Computer
{
public:
Computer(CPU * cpu, GPU * gpu, Memory * mem) //调用时父类指针指向子类对象,发生多态
{
m_cpu = cpu;
m_gpu = gpu;
m_mem = mem;
}
void doWork()
{
m_cpu->Calculate();
m_gpu->Display();
m_mem->Storage();
}
private:
CPU * m_cpu;
GPU * m_gpu;
Memory * m_mem;
};
/*-----------------创建Intel的三个零件类-----------------*/
class IntelCPU : public CPU
{
void Calculate()
{
cout << "Intel的CPU开始计算!" << endl;
}
};
class IntelGPU : public GPU
{
void Display()
{
cout << "Intel的GPU开始显示!" << endl;
}
};
class IntelMemory : public Memory
{
void Storage()
{
cout << "Intel的内存条开始存储!" << endl;
}
};
/*-----------------创建AMD的三个零件类-----------------*/
class AMDCPU : public CPU
{
void Calculate()
{
cout << "AMD的CPU开始计算!" << endl;
}
};
class AMDGPU : public GPU
{
void Display()
{
cout << "AMD的GPU开始显示!" << endl;
}
};
class AMDMemory : public Memory
{
void Storage()
{
cout << "AMD的内存条开始存储!" << endl;
}
};
void test01()
{
//组装一个全是Inel零件的电脑
IntelCPU Icpu;
IntelGPU Igpu;
IntelMemory Imem;
Computer c1(&Icpu, &Igpu, &Imem);
c1.doWork();
cout << "-----------------------------" << endl;
//组装一个全是AMD零件的电脑
AMDCPU AMDcpu;
AMDGPU AMDgpu;
AMDMemory AMDmem;
Computer c2(&AMDcpu, &AMDgpu, &AMDmem);
c2.doWork();
}
int main()
{
test01();
return 0;
}
结果:
分析:
在上述过程中,新建变量可以通过
new
的方式在堆区开辟变量,但是要记得要及时delete
,具体操作可以看原视频。
5 文件操作
程序在运行时产生的数据都是临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件<fstream>
文件类型分为两种:
1.文本文件 - 文件以文本的ASCII码形式存储在计算机中
2.二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类:
1.ofstream:写操作
2.ifstream:读操作
3.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 | 二进制方式 |
注意1:文件打开方式可以配合使用,利用|
操作符
例如,用二进制方式写文件ios::binary | ios::out
注意2:文件路径可以是绝对路径也可以是相对路径,只写文件名将保存在cpp文件的同级文件夹
示例:
#include<iostream>
#include<fstream> //1.包含 文件操作 头文件
using namespace std;
void test01()
{
//2.创建流对象
ofstream ofs;
//3.打开文件
ofs.open("./test.txt",ios::out);
//4.写数据
ofs << "姓名:张三" << endl << "年龄:18" << endl << "性别:男" << endl;
//5.关闭文件
ofs.close();
}
int main()
{
test01();
return 0;
}
结果:
在cpp文件同级目录下生成了.txt
文件,文件内容:
5.1.2 读文件
读文件与写文件步骤相似,但是读取方式相对比较多
读文件步骤如下:
- 包含头文件
#include<fstream>
- 创建流对象
ifstream ifs
- 打开文件并判断文件是否打开成功
ifs.open("文件路径", 打开方式)
- 读数据
四种方式操作 - 关闭文件
ifs.close()
判断文件是否打开成功:
类ifstream
里有一个函数.is_open()
用来判断文件是否打开成功,判断时,可用语句:
if(!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
四种读数据的方式
-
第一种方式
char buf[1024] = {0}; //创建一个大小为1024的字符数组,并且全部初始化为0 while(ifs >> buf) //通过右移运算符将ifs的数据存放到buf中,当读取完所有数据的时候,会返回一个假的结果,结束循环 { cout << buf << endl; }
-
第二种方式
char buf[1024] = {0}; while(ifs.getline(buf,sizeof(buf))) //用ifs的成员函数,第一个参数是数组指针,第二个参数是读取的长度,这里选一个较大值即可 { cout << buf << endl; }
-
第三种方式
string buf; while(getline(ifs,buf)) //这次使用全局函数getline(),每次读一行 { cout << buf << endl; }
-
第四种方式
char c; while( (c = ifs.get() ) != EOF ) //一个字符一个字符的读,直到读到了文件尾 { cout << c ; }
示例:
(读取的文件是5.1.1中写的那个文件)
#include<iostream>
#include<string>
#include<fstream> //1.包含 文件操作 头文件
using namespace std;
void test01()
{
//2.创建流对象
ifstream ifs;
//3.打开文件
ifs.open("./test.txt",ios::in);
//4.写数据
//第一种方法
// char buf[1024] = {0}; //创建一个大小为1024的字符数组,并且全部初始化为0
// while(ifs >> buf) //通过右移运算符将ifs的数据存放到buf中,当读取完所有数据的时候,会返回一个假的结果,结束循环
// {
// cout << buf << endl;
// }
//第二种方法
// char buf[1024] = {0};
// while(ifs.getline(buf,sizeof(buf)))
// {
// cout << buf << endl;
// }
//第三种方法
string buf;
while(getline(ifs,buf)) //这次使用全局函数getline(),每次读一行
{
cout << buf << endl;
}
//第四种方法
// char c;
// while( (c = ifs.get() ) != EOF ) //一个字符一个字符的读,直到读到了文件尾,结束循环
// {
// cout << c ;
// }
//5.关闭文件
ifs.close();
}
int main()
{
test01();
return 0;
}
总结:
- 前三种读取文件的方法,while括号里面的操作都是每次读取一行赋给字符数组/字符串(不读取换行符),当读到文件尾时,返回假,结束循环
- 第四种方法,while括号里面的操作每次只读取一个字符,包括读取换行符,所以在打印的时候不能添加endl。
5.2 二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为ios::binary
5.2.1 二进制写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char * buffer, int len);
函数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。
示例:
#include<iostream>
#include<fstream> //1.包含 文件操作 头文件
using namespace std;
class Person
{
public:
char m_name[48]; //写二进制的时候最好不要用C++的string,而是直接用字符数组
int m_age;
};
void test01()
{
//2.创建流对象
ofstream ofs("BinaryTest.txt",ios::out | ios::binary); //也可以直接写在第二部,省略第三步,这是一个构造函数
//3.打开文件
// ofs.open("BinaryTest.txt",ios::out | ios::binary); //二进制新文件,将两种打开方式用位或操作符连接
//4.写数据
Person p = {"张三", 18}; //将这个类变量写入文件
ofs.write((const char *)&p, sizeof(Person)); //将p的地址强制转化为const char *的类型
//5.关闭文件
ofs.close();
}
int main()
{
test01();
return 0;
}
结果:
生成了BinaryTest.txt文件,打开有乱码,因为是二进制的形式,问题不大,只要在下一节的二进制读文件中能读取到即可,如下图:
5.2.2 二进制读文件
二进制方式写文件主要利用流对象调用成员函数read
函数原型:istream& read(char * buffer, int len);
函数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。
示例:
#include<iostream>
#include<fstream> //1.包含 文件操作 头文件
using namespace std;
class Person
{
public:
char m_name[48]; //写二进制的时候最好不要用C++的string,而是直接用字符数组
int m_age;
};
void test01()
{
//2.创建流对象
ifstream ifs; //也可以直接写在第二部,省略第三步,这是一个构造函数
//3.打开文件
ifs.open("BinaryTest.txt",ios::in | ios::binary); //二进制读文件,将两种打开方式用位或操作符连接
if(!ifs.is_open()) //判断文件是否打开
{
cout << "无法打开文件" << endl;
return;
}
//4.读数据数据
Person p; //准备一个接数据的变量
ifs.read((char *)&p, sizeof(Person)); //将p的地址强制转化为char *的类型
//验证是否读到数据
cout << "姓名: " << p.m_name << " 年龄: " << p.m_age << endl;
//5.关闭文件
ifs.close();
}
int main()
{
test01();
return 0;
}
结果: