程序的内存模型
C++程序在执行时,内存大致分为四个区域:
代码区:存放函数体的二进制代码,由操作系统进行管理的。
全局区:存放全局变量和静态变量以及常量。
栈区:由编译器自动分配释放,存放函数的参数值、局部变量等。
堆区:由程序员分配和释放,若程序员不释放,程序结束操作系统回收。
内存四区的意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
代码区和全局区都是在程序运行之前,栈区和堆区是在程序运行之后;
内存四区
代码区
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码。
- 代码区是只读的,使其只读的原因是防止程序意外的修改了它的指令。
全局区
- 存放全局变量和静态变量
- 全局区还包含常量区:字符串常量和其他常量也存放在此
- 全局区的数据在程序结束之后由操作系统释放
全局变量:
静态变量:普通变量前面加 static 关键字的都是静态变量
常量:
- 字符串常量:带双引号(“”)的都是字符串常量
const
修饰的变量:const修饰的全局变量和局部变量
栈区
局部变量:函数体内的定义的变量都叫局部变量。
Tips:不要返回局部变量的地址,栈区开辟的数据由编译器分配和自动释放。
栈区存放 形参数据和局部变量
堆区
在C++中主要利用new
在堆区开辟内存
堆区开辟的数据,由程序员手动开辟、手动释放,释放利用关键字 delete
语法:new
数据类型
用new
创建的数据,会返回该数据对应的类型的指针。
int * func()
{
// 指针本身就是局部变量,指针保存在栈上
int *p = new int(10); // 利用new关键字将数据开辟到堆区
return p;
}
void func1()
{
int * arr = new int[10]; // 开辟拥有10个元素的数组,返回数组的首地址
for(int i; i < 10; i++)
{
arr[i] = i+100;
}
// 释放数组用delete[]
delete[] arr;
}
int main()
{
int *p = func(); // 在堆区开辟数据
cout << *p << endl; // 可以正常打印
delete p; // 释放数据
cout << *p << endl; // 不可以正常打印,会引发异常
}
C++中的引用
引用的基本使用
作用:给变量起别名
语法:数据类型
&别名
= 原名
;
引用的注意事项:
- 引用必须初始化
- 引用在初始化之后,不可以改变
int a = 10;
int &b = a; // int &b; 是错误的
int c = 20;
b = c; // 是赋值操作,不是更改引用。
//int &b = c; //是错误的,引用初始化后,不可以改变。编译时会报错
引用做函数参数
作用:函数传参数时,可以利用引用的技术让形参修饰实参。
优点:可以简化指针修改实参。
// 引用传递
void func(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 20;
func(a, b);
cout << "a的值: " << a << endl; // a = 20
cout << "b的值: " << b << endl; // b = 10
}
引用做函数的返回值
作用:引用可以作为函数的返回值存在的
tips:不要返回局部变量引用
用法:函数调用作为左值
int& func1()
{
static int a = 10;
return a;
}
int mian()
{
int &ref = func1();
}
引用的本质
引用的本质在C++内部实现是一个指针常量
指针常量是指针不可更改,这也是引用不可更改的原因。
int a = 10;
int *ref = a; // 自动转换为int * const ref = a;
ref = 20; // 编译器内部发现ref是引用,自动转换为 *ref = 20;
常量引用
作用:常量引用主要用来修饰形参,防止误操作。
在函数参数列表中,可以加const
来修饰形参,防止形参改变实参。
void func(const int &a)
{
cout << a << endl;
}
int main()
{
int &ref = 10; //这是错误语句,因为引用本身需要一个合法的内存空间
const int &ref = 10; //这是正确的,编译器优化代码:int tmp = 10; const int &ref = tmp;
}
函数高级
形参:函数声明时定义的参数,用于接收调用函数时传入的实参。形参只存在于函数定义中,不能在函数外部使用。
形参变量只有在被调用时才分配内存单元,调用结束释放所分配的内存单元。
实参:调用函数时传递给函数的参数,实参的值会被传递给函数的形参。实参出现在主调函数中,进入被调函数后,实参就不能够用了。
每次调用函数时,都会重新创建该函数所有的形参,此时所传递的实参将会初始化对应的形参。
函数的默认参数、占位参数
函数的默认参数
语法:返回值类型
函数名
(参数
=数值
,…) {}
- 默认参数放在参数列表的最后,即默认参数之后的参数都得是默认参数
- 如果函数声明有默认参数,那么函数定义就不能有默认参数
int func(int a= 0, int b = 5)
{
return a+b;
}
int main()
{
cout << func() << endl; // 输出 5
cout << func(2) << endl; // 输出 7
cout << func(2, 9) << endl; // 输出 11
}
函数的占位参数
c++中函数的形参列表中可以有占位参数,可以用来占位,调用函数时必须填补该位置。
语法:返回值类型
函数名
(数据类型
) {}
void func1(int a, int)
{
cout << "func1" << endl;
}
void func2(int a, int = 20) // 占位参数可以有默认参数
{
cout << "func2" << endl;
}
int main()
{
int a = 10, b = 20;
func1(a, b);
func2(a);
}
函数的重载
作用:函数名可以相同,提高复用性。
函数重载满足的条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型或个数不同或 顺序不同
tips: 函数的返回值不可以作为函数重载的条件
void func()
{
cout << "func()" << endl;
}
void func(int a) // 参数个数不同
{
cout << "func(int a)" << endl;
}
void func(double a)
{
cout << "func(float a)" << endl; // 参数类型不同
}
void func(int a, double b)
{
cout << "func(int a, float b)" << endl;
}
void func(double b, int a)
{
cout << "func(float b, int a)" << endl;
}
int main()
{
func(10); // func(int a)
func(3.14); // func(double a)
func(10,3.14) // func(int a, double b)
func(3.14, 10) // func(double b, int a)
}
引用作为重载的条件
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(int &a)
func(10); // 会调用 func(const int &a)
/* 因为对于func(10),如果走func(int &a),相当于:int &a = 10, 这是不合法的;如果走func(const int &a),相当于:const int &a = 10, 这是合法的*/
}
函数重载遇到默认参数
尽量避免此情况
类和对象
C++面向对象的三大特性:封装、继承、多态(也是所有面向对象语言的三大特性)
面向对象语言认为万事万物皆为对象,对象有属性和行为。
C++、python、java都是面向对象语言。
封装
封装的意义
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法:class
类名
{ 访问权限
:属性/行为
};
class Student
{
public:
string names;
int stu_num;
string setName(string name)
{
names = name;
return names;
}
int setNum(int num)
{
stu_num = num;
return stu_num;
}
void showStudent()
{
cout << "name: " << names << "\tstu_num: " << stu_num << endl;
}
int main()
{
Student s1;
s1.names = "张三";
s1.stu_num = 7382;
s1.showStudent();
cout << "--------------" << endl;
s1.setName("李四");
s1.setNum(88);
s1.showStudent();
}
访问权限有三种:
public
公共权限 (类内可以访问,类外也可以访问)protected
保护权限 (类内可以访问,类外不可以访问;子类可以访问父类的保护内容)private
私有权限 (类内可以访问,类外不可以访问;子类不可以访问父类的私有内容)
struct与class的区别
在C++中struct和class唯一区别就在于默认的访问权限不同.
区别:
struct 默认权限为公开
class 默认权限为私有
成员属性设置为私有
优点:
- 将所有成员设置为私有,可以自己控制读写权限
- 对于写权限,可以检测数据的有效性
案例1
立方体类:计算立方体的面积和体积,并通过全局函数和成员函数判断两个立方体是否一样。
#include <iostream>
#include <string>
using namespace std;
class Cube
{
private:
float m_L=3, m_W=4, m_H=2;
public:
void setH(float h) //设置高
{
m_H = h;
}
void setL(float l) //设置长
{
m_L = l;
}
void setW(float w) //设置宽
{
m_W = w;
}
float getH() //获取高
{
return m_H;
}
float getL() //获取长
{
return m_L;
}
float getW() //获取宽
{
return m_W;
}
float surface_area() //获取面积
{
return getH() * getL() *2 + getL() * getW() *2 + getH() * getW() *2;
}
float volume() //获取体积
{
return getH() * getL() * getW();
}
// 利用成员函数判断两个立方体是否一样
bool isSame(Cube &c)
{
if (m_H == c.getH() && m_L == c.getL() && m_W == c.getW())
{
return true;
}
return false;
}
};
bool isEqual(Cube &c1, Cube &c2)
{
if (c1.getH() == c2.getH() && c1.getL() == c2.getL() && c1.getW() == c2.getW())
{
return true;
}
return false;
}
int main()
{
Cube C1;
C1.setH(10);
C1.setL(10);
C1.setW(10);
cout << "表面积1:" << C1.surface_area() << " 体积1:" << C1.volume() << endl;
Cube C2;
C2.setH(10);
C2.setL(10);
C2.setW(100);
cout << "表面积2:" << C2.surface_area() << " 体积2:" << C2.volume() << endl;
int ret1 = isEqual(C1, C2);
if (ret1)
{
cout << "全局函数判断: C1和C2一样" << endl;
}
else
{
cout << "全局函数判断: C1和C2不一样" << endl;
}
int ret2 = C2.isSame(C1);
if (ret2)
{
cout << "成员函数判断: C1和C2一样" << endl;
}
else
{
cout << "成员函数判断: C1和C2不一样" << endl;
}
}
案例2
点和圆的关系:设计一个圆形类(Circle)和一个点类(Point),计算点和圆的关系。
#include <iostream>
#include <string>
using namespace std;
class Point
{
private:
int x, y;
public:
void setPx(int x_)
{ x = x_;}
void setPy(int y_)
{ y = y_;}
int getPx()
{ return x; }
int getPy()
{ return y; }
};
class Circle
{
private:
int c_R;
Point centre;
public:
void setRadius(int rr)
{ c_R = rr; }
int getRadius()
{ return c_R; }
void setCentre(Point p)
{ centre = p; }
Point getCenter()
{ return centre;}
};
//判断点与圆的关系
void relation(Point &p, Circle &cc)
{
// 计算两点之间的距离
int distance = (cc.getCenter().getPx()-p.getPx())*(cc.getCenter().getPx()-p.getPx()) +
(cc.getCenter().getPy()-p.getPy())*(cc.getCenter().getPy()-p.getPy());
int rDistance = cc.getRadius()*cc.getRadius();
if (distance == rDistance)
{
cout << "点在圆上" << endl;
}
else if (distance < rDistance)
{
cout << "点在圆内" << endl;
}
else
{
cout << "点在圆外" << endl;
}
}
int main()
{
Circle c1;
c1.setRadius(5);
Point center;
center.setPx(5);
center.setPy(4);
c1.setCentre(center);
Point p1;
p1.setPx(8);
p1.setPy(8);
relation(p1, c1);
}
对象的初始化和清理
构造函数和析构函数
对象的初始化和清理是两个非常重要的安全问题
- 一个对象或变量没有初始状态,对其使用后果是未知的
- 使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++使用构造函数和析构函数解决了以上问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供;只不过编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名()
{}
- 构造函数没有返回值也不写
void
- 函数名称和类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用构造函数,无需手动调用,而且只会调用一次
析构函数语法:~类名()
{}
- 析构函数没有返回值也不写
void
- 函数名称和类名相同,在名称前面加上“~”符号
- 析构函数不可以有参数,所以不可以发生重载
- 程序在对象销毁之前会自动调用析构函数,无需手动调用,而且只会调用一次
构造函数的分类及调用
两种分类方式:
- 按参数分类为:有参构造和无参构造
- 按类型分类为:普通构造和拷贝构造
三种调用方式:括号法、显示法、隐式转换法
#include <iostream>
using namespace std;
class Point
{
private:
/* data */
public:
int age = 8;
// 构造函数
Point(int a)
{
int num = a;
cout << "有参构造函数, 参数:" << num << endl;
}
Point()
{
cout << "无参构造函数" << endl;
}
Point(const Point &p)
{
// 将传入的对象的所有属性拷贝至此对象上
int age = p.age;
cout << "拷贝构造函数,参数:" << age << endl;
}
// 析构函数
~Point()
{
cout << "析构函数" << endl;
}
};
int main()
{
/* 括号法 */
Point p1; //无参(默认)构造函数调用【不加()】
Point p2(10); //有参构造函数调用
Point p3(p2); //拷贝构造函数调用
/* 显示法 */
Point p4;
Point p5 = Point(20); //【有参构造】
Point p6 = Point(p5); //【拷贝构造】
Point(20); //匿名对象。特点:当前行执行结束后,系统会立即回收掉匿名对象
// 不要利用拷贝构造函数 初始化匿名对象,编译器会认为 Point(p3)===Point p3
/* 隐式转换法 */
Point p7 = 10; //相当于Point p7 = Point(10); 【有参构造】
Point p8 = p7; //相当于Point p8 = Point(p7); 【拷贝构造】
}
拷贝构造函数的调用时机
通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
示例类:Person
#include <iostream>
using namespace std;
class Person
{
private:
/* data */
public:
int m_Age = 10;
Person() // 普通构造函数
{
cout << "无参构造函数" << endl;
}
// 有参构造函数
Person(int a)
{
m_Age = a;
cout << "有参构造函数" << m_Age << endl;
}
// 拷贝构造函数
Person(const Person &p)
{
m_Age = p.m_Age;
cout << "拷贝构造函数" << m_Age << endl;
}
// 析构函数
~Person()
{
cout << "析构函数" << endl;
}
};
#include <iostream>
using namespace std;
/* 使用“示例类:Person”*/
// 1.使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
Person p1(20);
Person p2(p1);
cout << "p2的年龄: " << p2.m_Age << endl;
}
// 2.值传递的方式给函数参数传值
void doWork(Person p)
{
}
void test02()
{
Person p;
doWork(p);
}
// 3.值方式返回局部对象
Person work()
{
Person p;
return p;
}
void test03()
{
Person p = work();
}
int main()
{
test01();
test02();
test03();
}
构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不再提供其他构造函数
#include <iostream>
using namespace std;
class Person
{
private:
/* data */
public:
int m_Age = 10;
// 有参构造函数
Person(int a)
{
m_Age = a;
cout << "有参构造函数" << m_Age << endl;
}
// 析构函数
~Person()
{
cout << "析构函数" << endl;
}
};
void test01()
{
Person p(18);
Person p1(p);
cout << "" << p1.m_Age << endl;
}
int main()
{
test01();
}
/* 输出结果:
有参构造函数18
p1的年龄:18
析构函数
析构函数
*/
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
利用编译器提供的拷贝构造函数,会做浅拷贝操作。
#include <iostream>
using namespace std;
class Person
{
public:
int m_Age;
int *m_height;
Person()
{
cout << "无参构造函数" << endl;
}
// 构造函数
Person(int a, int height)
{
m_Age = a;
m_height = new int(height);
cout << "有参构造函数" << endl;
}
// 拷贝构造函数
Person(const Person &p)
{
cout << "拷贝构造函数" << endl;
// 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_Age = p.m_Age;
// m_height = p.m_height; 编译器默认实现的就是这行代码
m_height = new int(*p.m_height); // 深拷贝操作
}
// 析构函数
~Person()
{
cout << "析构函数" << endl;
// 释放在堆区开辟的空间
if (m_height != NULL)
{
delete m_height;
}
}
};
void test01()
{
Person p1(20, 170);
cout << "p1的年龄: " << p1.m_Age << " p1的身高: " << *p1.m_height << endl;
Person p2(p1);
cout << "p2的年龄: " << p2.m_Age << " p2的身高: " << *p2.m_height << endl;
}
int main()
{
test01();
}
/* 输出结果
有参构造函数
p1的年龄: 20 p1的身高: 170
拷贝构造函数
p2的年龄: 20 p2的身高: 170
析构函数
析构函数
*/
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题(问题:堆区的内存重复释放)。
初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数()
:属性1(值1)
,属性2(值2)
…{}
#include <iostream>
using namespace std;
class Person
{
public:
int m_A, m_B;
// 传统初始化操作
// Person(int a, int b)
// {
// m_A = a;
// m_B = b;
// }
// 初始化列表初始化属性
Person(int a, int b): m_A(a), m_B(b)
{
/* code or null*/
}
};
void test01()
{
// Person p(30,60);
Person p(50, 20);
cout << p.m_A << endl;
}
int main()
{
test01();
}
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为“对象成员”。
#include <iostream>
#include <string>
using namespace std;
class Phone
{
public:
string m_PName;
Phone(string pName)
{
m_PName = pName;
cout << "Phone的构造函数调用" << endl;
}
~Phone()
{
cout << "Phone的析构函数调用" << endl;
}
};
class Person
{
public:
string m_Name;
Phone m_Phone; // 对象成员
Person(string name, string pName): m_Name(name), m_Phone(pName)
{
cout << "Person的构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
void test01()
{
Person p("李四", "三星");
cout << p.m_Name << "拿着" << p.m_Phone.m_PName << "手机." << endl;
}
int main()
{
test01();
}
/* 输出结果
Phone的构造函数调用
Person的构造函数调用
李四拿着三星手机.
Person的析构函数调用
Phone的析构函数调用
*/
总结:
构造函数的调用顺序:先调用对象成员(Phone)的构造函数,再调用自身(Person)的构造函数。
析构函数的调用顺序:先调用自身(Person)的析构函数,再调用对象成员(Phone)的析构函数。[与构造函数相反]
静态成员
在成员变量和成员函数之前加关键字static
,称之为静态成员。
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
#include <iostream>
#include <string>
using namespace std;
class Person
{
// 类外不可以访问私有静态成员变量
private:
static int m_B;
static void func2()
{
cout << "static void func2调用" << endl;
}
public:
static int m_A; // 类内声明
int m_C;
static void func()
{
m_A = 100;
// m_C = 300; // 静态成员函数不能访问非静态成员变量
cout << "static void func调用" << endl;
}
};
int Person::m_A = 10; // 类外初始化
int Person::m_B = 20;
void test01()
{
/* 静态成员变量不属于某个对象上,所有对象都共享同一份数据,因此静态成员变量有两种访问方式*/
// 1.通过对象进行访问静态成员变量
Person p;
cout << "p.m_A: " << p.m_A << endl; // p.m_A=10
Person p2;
p2.m_A = 20;
cout << "修改过后 p.m_A: " << p.m_A << endl; // p.m_A=20
// 2.通过类名访问静态成员变量
cout << "通过类名访问静态成员变量:" << Person::m_A << endl;
// cout << "访问类私有成员" << Person::m_B << endl; // 会提示"m_B"不可访问
}
void test02()
{
//1.通过对象进行访问静态成员函数
Person p;
p.func();
//2.通过类名访问静态成员函数
Person::func();
// Person::func2(); // 类外不能访问私有静态成员函数
}
int main()
{
test01();
test02();
}
C++对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
C++编译器会为每个空对象分配1个字节空间,是为了区分空对象占内存的位置。每个空对象都有个独一无二的内存地址。
#include <iostream>
using namespace std;
class Kong
{
};
class Person
{
int m_a; // 非静态成员变量
};
class Phone
{
static int m_c; // 静态成员变量
static void func(){} // 静态成员函数
};
int Phone::m_c = 10;
class Person2
{
int m_a; // 非静态成员变量
void func(){} // 非静态成员函数
};
void test00()
{
Kong k;
cout << "size of k: " << sizeof(k) << endl; // size of k: 1
}
void test01()
{
Person p;
cout << "size of p: " << sizeof(p) << endl; // size of p: 4, 非静态成员属于对象上
}
void test02()
{
Phone pe;
cout << "size of pe: " << sizeof(pe) << endl; // size of pe: 1, 静态成员和函数不属于类的对象上,所以不占类对象内存
}
void test03()
{
Person2 pp;
cout << "size of pp: " << sizeof(pp) << endl; // size of pp: 4, 成员变量和成员函数是分开存储的
}
int main()
{
test00();
test01();
test02();
test03();
}
this指针概念
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的本质:是指针常量,指针的指向不可以更改
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分【解决名称冲突】
- 在类的非静态成员函数中返回对象本身,可使用return *this
#include <iostream>
using namespace std;
class Person
{
public:
int age;
Person(int age)
{
this -> age = age;
// age = age;
}
// 返回引用,不是返回值。返回值的话,每次调用都会创建一个新的对象
Person& PersonAddAge(Person &p)
{
this -> age += p.age;
// this指向p3的指针,而*this指向的就是p3这个对象本身
return *this;
}
};
// 1.解决名称冲突问题
void test01()
{
Person p1(18);
cout << "p1的年龄: " << p1.age << endl;
}
// 2.返回对象本身用*this
void test02()
{
Person p2(10);
Person p3(20);
p3.PersonAddAge(p2);
cout << "p3的年龄: " << p3.age << endl; // 30
// 链式编程思想
p3.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2);
cout << "now, p3的年龄: " << p3.age << endl; // 60
}
int main()
{
test02();
}
空指针访问成员函数
C++中空指针也可以调用成员函数,但要注意有没有用到this指针;如果用到this指针,需要加以判断保证代码的健壮性。
#include <iostream>
using namespace std;
class Person
{
public:
int m_Age;
void showPersonName()
{
cout << "this is Person class" << endl;
}
void showPersonAge()
{
if (this == NULL)
{
return;
}
// 以上代码是为了避免出现空指针的
cout << "age = " << m_Age << endl;
}
};
void test01()
{
Person *p = NULL;
p->showPersonName();
p->showPersonAge(); //如果不加if语句判断,会报错,因为传入的指针为NULL
}
int main()
{
test01();
}
const修饰成员函数
常函数:
- 成员函数后 加
const
我们称这个函数为常函数 - 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable
后,在常函数中依然可以修改
常对象:
- 声明对象前加
const
称该对象为常对象 - 常对象只能调用常函数
Tips:在成员函数后加const
,修饰的是this指针指向,让指针指向的值也不可以更改
#include <iostream>
using namespace std;
class Person
{
public:
int m_A;
mutable int m_B; //特殊变量,加mutable关键字,即使在常函数中也可以修改这个变量
Person()
{
}
void showPerson1() //相当于 Person * const this
{
this->m_A = 10;
}
//在成员函数后加const,修饰的是this指针指向,让指针指向的值也不可以更改
void showPerson2() const //相当于 const Person * const this
{
// this->m_Age = 10; //会报错
this->m_B = 20;
}
};
void test01()
{
Person *p = NULL;
}
void test02()
{
const Person p2; // 常对象
// p2.m_A = 100; // 会报错,不可修改
p2.m_B = 200; // m_B是特殊值,在常对象下可以修改
p2.showPerson2(); // 常对象只能调用常函数
// p2.showPerson1(); // 会报错,常对象不能调用非常函数
}
int main()
{
test01();
test02();
}
友元
友元的作用就是让一个函数或类 访问另一个类的私有成员
友元的关键字是 friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
#include <iostream>
#include <string>
/* 全局函数做友元 */
using namespace std;
class Building
{
// 声明goodGay全局函数为友元
friend void goodGay(Building &build); // 全局函数做友元
private:
string m_BedRoom;
public:
string m_SittingRoom;
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
};
//全局函数
void goodGay(Building &build)
{
cout << "好基友全局函数 访问:" << build.m_SittingRoom << endl;
cout << "好基友全局函数 访问:" << build.m_BedRoom << endl; // 不声明goodGay为友元的情况下,此行代码会报错
}
void test01()
{
Building build;
goodGay(build);
}
int main()
{
test01();
}
类做友元
#include <iostream>
#include <string>
/* 类做友元 */
using namespace std;
class Building
{
friend class GoodGay; // 类做友元
private:
string m_BedRoom;
public:
string m_SittingRoom;
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
};
class GoodGay
{
public:
Building *building;
GoodGay()
{
building = new Building;
}
// void visit(); // 此函数访问Building中的属性
void visit()
{
cout << "好基友全局函数 访问:" << building->m_SittingRoom << endl;
cout << "好基友全局函数 访问:" << building->m_BedRoom << endl;
}
};
void test02()
{
GoodGay gg;
gg.visit();
}
int main()
{
test02();
}
成员函数做友元
#include <iostream>
#include <string>
using namespace std;
class Building;
class goodGay
{
private:
Building *building;
public:
goodGay();
void visit(); // 此函数可以访问Building中的私有属性
void visit2(); // 此函数不可以访问Building中的私有属性
};
class Building
{
friend void goodGay::visit(); // 成员函数做友元
private:
string m_BedRoom;
public:
string m_SittingRoom;
Building();
};
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{
building = new Building;
}
void goodGay::visit()
{
cout << "visit函数 访问:" << building->m_SittingRoom << endl;
cout << "visit函数 访问:" << building->m_BedRoom << endl;
}
void goodGay::visit2()
{
cout << "visit2函数 访问:" << building->m_SittingRoom << endl;
// cout << "visit2函数 访问:" << building->m_BedRoom << endl; // 不能访问私有属性
}
void test01()
{
goodGay gg;
gg.visit();
gg.visit2();
}
int main()
{
test01();
}
C++运算符重载
运算符重载概念:对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型。
加号运算符重载
作用:实现两个自定义数据类型相加的运算
- 成员函数重载加号
#include <iostream>
using namespace std;
class Person
{
public:
int m_A;
int m_B;
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;
}
};
void test01()
{
Person p1;
p1.m_A = 10;
p1.m_B = 20;
Person p2;
p2.m_A = 10;
p2.m_B = 20;
Person p3 = p1 + p2; // 成员函数重载本质调用: Person p3 = p1.operator+(p2);[与此行代码等效]
cout << "p3.m_A:" << p3.m_A << " p3.m_B: " << p3.m_B << endl;
}
int main()
{
test01();
}
- 全局函数重载加号
#include <iostream>
using namespace std;
class Person
{
public:
int m_A;
int m_B;
};
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 operator+(Person &p1, int num)
{
Person temp;
temp.m_A = p1.m_A + num;
temp.m_B = p1.m_B + num;
return temp;
}
void test01()
{
Person p1;
p1.m_A = 10;
p1.m_B = 20;
Person p2;
p2.m_A = 10;
p2.m_B = 20;
Person p3 = p1 + p2; // 全局函数重载本质调用:Person p3 = operator+(p1, p2); [与此行代码等效]
cout << "p3.m_A:" << p3.m_A << " p3.m_B: " << p3.m_B << endl;
Person p4 = p1 + 5;
cout << "p4.m_A:" << p3.m_A << " p4.m_B: " << p3.m_B << endl; // 输出结果:p4.m_A: 15 p4.m_B: 25
}
int main()
{
test01();
}
总结:
- 内置的数据类型的表达式的运算符是不可能改变的。
- 不要滥用运算符重载
左移运算符重载
作用:可以输出自定义数据类型
只能利用全局函数重载左移运算符
class Person
{
public:
int m_A;
int m_B;
};
// 只能利用全局函数重载左移运算符
// “ostream&” 链式编程
ostream& operator<<(ostream &cout, Person &p) //本质:operator<<(cout, p) 简化为 cout << p
{
cout << "m_A:" << p.m_A << " m_B: " << p.m_B << endl;
return cout; // 链式编程
}
int main()
{
Person p;
p.m_A = 10;
p.m_B = 20;
cout << p << endl; // 输出为 m_A:10 m_B:20
}
递增运算符重载 [与递减预算符雷同:++改成- -]
作用:通过重载递增运算符,实现自己的整型数据
#include <iostream>
using namespace std;
class MyInteger
{
friend ostream& operator<<(ostream &cout, const MyInteger &myData);
private:
int m_Num;
public:
MyInteger()
{
m_Num = 0;
}
// 重载前置++运算符 [前置:先自增,再参与运算]
MyInteger& operator++() //返回引用而不是返回值的原因:一直对一个数据进行递增操作
{
++m_Num; // 先进行++运算
return *this; // 再将自身做返回
}
// 重载后置++运算符 [后置:先参与运算,再自增]
MyInteger operator++(int) // int代表占位参数,用于区分前置和后置递增
{
MyInteger temp = *this; // 先 记录当时结果
m_Num++; // 后 递增
return temp; // 最后将记录结果返回
}
};
// 重载<<运算符
ostream& operator<<(ostream &out, const MyInteger &myData) // 加“const”的原因:C++不允许非常量的临时引用作为参数
{
out << myData.m_Num << endl;
return out;
}
void test01()
{
MyInteger myint;
cout << myint << endl;
cout << ++(++myint) << endl;
cout << myint << endl;
}
void test02()
{
MyInteger myInt;
cout << myInt << endl; // 输出0
cout << myInt++ << endl;// 输出1
cout << myInt << endl; // 输出1
myInt++;
cout << myInt << endl; // 输出2
}
int main()
{
test01();
cout << "------" << endl;
test02();
}
赋值运算符重载
C++编译器至少给一个类添加4个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。
#include <iostream>
using namespace std;
class Person
{
public:
int *m_Age;
Person(int age)
{
m_Age = new int(age);
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
// 重载 赋值操作符
Person& operator=(Person &p)
{
// m_Age = p.m_Age; //编译器是提供浅拷贝
//应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
if(m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
// 深拷贝
m_Age = new int(*p.m_Age);
return *this;
}
};
void test01()
{
Person p1(18);
Person p2(20);
Person p3(28);
p3 = p2 = p1; // 赋值操作
cout << "p1的年龄: " << *p1.m_Age << endl;
cout << "p2的年龄: " << *p2.m_Age << endl;
cout << "p3的年龄: " << *p3.m_Age << endl;
}
int main()
{
test01();
}
关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string m_Name;
int m_Age;
Person(string name, int age)
{
m_Name = name;
m_Age = age;
}
// 重载 == 运算符
bool operator==(Person &p) // , Person &p2
{
if (this->m_Age == p.m_Age && this->m_Name == p.m_Name)
{
return true;
}
return false;
}
// 重载 != 运算符
bool operator!=(Person &p)
{
if (this->m_Age != p.m_Age || this->m_Name != p.m_Name)
{
return true;
}
return false;
}
};
void test01()
{
Person p1("tom", 18);
Person p2("tim", 18);
if (p1 == p2)
{
cout << "p1与p2相等" << endl;
}
else
{
cout << "p1与p2不相等" << endl;
}
if (p1 != p2)
{
cout << "p1与p2不相等" << endl;
}
else
{
cout << "p1与p2相等" << endl;
}
}
int main()
{
test01();
}
函数调用运算符重载
- 函数调用运算符"()"也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
#include <iostream>
#include <string>
using namespace std;
class MyPrint
{
public:
void operator()(string test)
{
cout << test << endl;
}
};
class MyAdd
{
public:
int operator()(int a, int b)
{
return a+b;
}
};
void test01()
{
MyPrint mp1;
mp1("hello");
}
void test02()
{
MyAdd myadd;
int num = myadd(10,10);
cout << num << endl;
}
int main()
{
test01();
test02();
}
继承
继承是面向对象三大特性之一
继承基本语法
语法:class
子类
:继承方式
父类
子类 也称为 派生类
父类 也称为 基类
#include <iostream>
#include <string>
using namespace std;
class BasePage
{
public:
void header()
{
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left()
{
cout << "Java、Python、C++...(公共分类列表)" << endl;
}
};
class Java:public BasePage
{
public:
void content()
{
cout << "Java学科视频" << endl;
}
};
class Python:public BasePage
{
public:
void content()
{
cout << "Python学科视频" << endl;
}
};
void test01()
{
cout << "Java下载视频页面如下: " << endl;
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();
cout << "Python下载视频页面如下: " << endl;
Python py;
py.header();
py.footer();
py.left();
py.content();
}
int main()
{
test01();
}
继承方式
- 公共继承
- 保护继承
- 私有继承
父类中的私有属性/成员,不可以被子类继承(无论哪种继承方式);
公共继承:public、protected权限,子类父类相同;
保护继承:父类的public、protected内容 -> 子类的protected内容
私有继承:父类的public、protected内容 -> 子类的private内容
继承中的对象模型
父类中所有的非静态成员属性都会被子类继承下去。
父类中私有成员,是被编译器隐藏了,所以访问不到,但确实会被子类继承。
#include <iostream>
using namespace std;
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son:public Base
{
public:
int m_D;
};
void test01()
{
Son s1;
cout << "size of Son = : " << sizeof(Son) << endl; // 输出:16 (4*4)
}
int main()
{
test01();
}
继承中的构造和析构顺序
子类继承父类,当子类创建对象时,也会调用父类的构造函数。
继承中构造的顺序:先构造父类再构造子类
继承中析构的顺序:先构造子类再构造父类(与构造顺序相反)
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base构造函数!" << endl;
}
~Base()
{
cout << "Base析构函数!" << endl;
}
};
class Son:public Base
{
public:
Son()
{
cout << "Son构造函数!" << endl;
}
~Son()
{
cout << "Son析构函数!" << endl;
}
};
void test01()
{
Son s1;
}
int main()
{
test01();
}
/* 输出结果:
Base构造函数!
Son构造函数!
Son析构函数!
Base析构函数! */
继承中同名成员的处理方式
当子类和父类中有同名的成员,如何通过子类对象访问到子类或父类中同名的数据呢?
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
#include <iostream>
using namespace std;
class Base
{
public:
int m_A;
Base()
{
m_A = 100;
}
void func()
{
cout << "Base - func" << endl;
}
};
class Son:public Base
{
public:
int m_A;
Son()
{
m_A = 200;
}
void func()
{
cout << "Son - func" << endl;
}
};
void test01()
{
Son s1;
cout << "Son m_A: " << s1.m_A << endl;
cout << "Base m_A: " << s1.Base::m_A << endl;
}
void test02()
{
Son s2;
s2.func(); // 直接调用 调用的是子类中的成员函数
s2.Base::func(); // 父类中的成员函数
}
int main()
{
test01();
test02();
}
继承中同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
只不过有两种访问方式:通过对象访问和通过类名访问
#include <iostream>
using namespace std;
class Base
{
public:
static int m_A;
static void func();
};
int Base::m_A = 100;
void Base::func()
{
cout << "Base - static func()" << endl;
}
class Son:public Base
{
public:
static int m_A;
static void func();
};
int Son::m_A = 200;
void Son::func()
{
cout << "Son - static func()" << endl;
}
void test01()
{
cout << "*****通过对象访问参数*****" << endl;
Son s1;
cout << "Son static m_A: " << s1.m_A << endl;
cout << "Base staic m_A: " << s1.Base::m_A << endl;
cout << "*****通过类名访问参数*****" << endl;
cout << "Son static m_A: " << Son::m_A << endl;
// 第一个::代表通过类名方式访问,第二个::代表访问父类作用域下
cout << "Base static m_A: " << Son::Base::m_A << endl;
}
void test02()
{
cout << "*****通过对象访问函数*****" << endl;
Son s1;
s1.func(); // 子类中的func
s1.Base::func(); // 父类中的func
cout << "*****通过类名访问函数*****" << endl;
Son::func(); // 子类中的func
// 第一个::代表通过类名方式访问,第二个::代表访问父类作用域下
Son::Base::func(); // 父类中的func
}
int main()
{
test01();
test02();
}
多继承语法
C++允许一个类继承多个类
语法:class
子类
:继承方式
父类1
,继承方式
父类2
,…
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
#include <iostream>
using namespace std;
class Base1
{
public:
int m_A;
Base1()
{
m_A = 100;
}
};
class Base2
{
public:
int m_B, m_A;
Base2()
{
m_B = 200;
m_A = 10;
}
};
class Son:public Base1, public Base2
{
public:
int m_C, m_D;
Son()
{
m_C = 300;
m_D = 400;
}
};
void test01()
{
Son s;
cout << "size of Son = " << sizeof(s) << endl; // 输出:20字节
cout << "Base1 m_A: " << s.Base1::m_A << endl; // 输出:100
cout << "Base2 m_A: " << s.Base2::m_A << endl; // 输出:10
}
int main()
{
test01();
}
菱形继承
菱形继承概念:
两个派生类(子类)继承同一个基类(父类),又有某个类同时继承这两个派生类,这种继承被称为菱形继承或钻石继承。
菱形继承典型案例:
菱形继承带来的问题:
- 羊继承了动物的数据,驼继承了动物的数据,当羊驼要使用数据时,就会产生二义性。
- 羊驼继承了两份动物的数据,但这份数据我们只需要一份即可。
解决方法:
两个父类拥有相同数据时,需要加作用域用以区分。
利用虚继承解决菱形继承有两份数据的问题:
- 继承方式之前加关键字“
virtual
”,变为虚基类; - 被继承的基类称为虚基类
#include <iostream>
using namespace std;
class Animals
{
public:
int m_Age;
};
class Sheep:virtual public Animals {};
class Tuo:virtual public Animals {};
class SheepTuo:public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 10;
cout << st.m_Age << endl; // 输出结果为:10
st.Tuo::m_Age = 20;
cout << st.m_Age << endl; // 如果不加virtual关键字,此行代码会报错。输出结果为:20 [数据被共享了]
}
int main()
{
test01();
}
虚继承原理:
在Sheep
和Tuo
类中使用virtual
关键字将Animals
类标记为虚基类。这将导致编译器在生成Sheep
和Tuo
类的对象时,为它们分配一个指向Animals
类的虚基类表的指针。这个虚基类表包含了Animals
类的地址偏移量,使得Sheep
和Tuo
类可以通过这个指针来访问Animals
类的成员。
当SheepTuo
类继承Sheep
和Tuo
类时,它会包含两个指向Animals
类虚基类表的指针,这些指针指向同一个虚基类表,这个虚基类表中包含了Animals
类的地址偏移量。这使得SheepTuo
类可以通过这些指针来访问Animals
类的成员,而不会出现数据重复和冗余的情况。
虚继承的原理是通过在派生类中维护一个指向虚基类表的指针,来实现对虚基类的间接访问,从而避免多重继承中的数据重复和冗余问题。
虚继承的实现需要在编译器中生成虚基类表,并在派生类中维护这个表的指针。由于虚继承需要额外的开销和复杂度,应该谨慎使用,只在必要的情况下才使用虚继承。
多态
多态的基本概念
多态是面向对象三大特性之一
多态分为两类:
- 静态多态:函数重载、运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
动态多态满足的条件:
- 有继承关系
- 子类重写父类的虚函数
重写:函数返回值类型、函数名、参数列表 完全一致才称为重写
动态多态的使用:
- 父类的指针或引用,指向子类对象
#include <iostream>
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;
}
};
void doSpeak(Animal &animal) // Animal &animal = cat or dog;
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat); // 如果不加virtual关键字,则输出:“动物在说话”[地址早绑定]。加了virtual关键字,输出:“猫在说话”[地址晚绑定]
Dog dog;
doSpeak(dog); // 输出:“狗在说话”
}
int main()
{
test01();
}
多态的原理剖析
C++中的多态性是通过虚函数(virtual functions)实现的。虚函数是在基类中声明的函数,可以在派生类中被重写。
在C++中,虚函数使用virtual
关键字来声明。使用虚函数时,如果一个指针或引用指向基类对象,但实际指向派生类对象,则在调用虚函数时将调用派生类中的版本。
在C++中,使用虚函数来实现多态性有两个关键点:
- 将基类函数声明为虚函数。
- 使用指向基类对象的指针或引用来调用虚函数。
#include <iostream>
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;
}
};
// 使用引用调用虚函数
void doSpeak(Animal &animal) // Animal &animal = cat or dog;
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat); // 如果不加virtual关键字,则输出:“动物在说话”[地址早绑定]。加了virtual关键字,输出:“猫在说话”[地址晚绑定]
Dog dog;
doSpeak(dog); // 输出:“狗在说话”
}
void test02()
{
cout << "size of Animal: " << sizeof(Animal) << endl;
cout << "size of Cat: " << sizeof(Cat) << endl;
cout << "size of Dog: " << sizeof(Dog) << endl;
}
// 使用指针调用虚函数
void test03()
{
Animal* a;
a = new Cat();
a->speak(); // 输出:“猫在说话”
a = new Dog();
a->speak(); // 输出:“狗在说话”
}
int main()
{
// test01();
// test02();
test03();
}
以test03为例:
Animal
类中的speak()
函数被声明为虚函数,派生类中的speak()
函数也被声明为虚函数,并且它们的函数签名相同。当调用a->speak()
时,编译器会根据a
指向的对象的实际类型,调用对应的speak()
函数。这种技术称为动态绑定(dynamic binding)。这是C++中实现多态性的基础。
多态案例 - 计算器类
案例描述:分别用普通技术和多态技术,设计实现两个操作数进行运算的计算器类
普通方法实现
#include <iostream>
#include <string>
using namespace std;
// 普通实现
class Calculator
{
private:
/* data */
public:
int m_Num1, m_Num2;
int getResult(string oper)
{
if (oper == "+")
{
return m_Num1 + m_Num2;
}
else if (oper == "-")
{
return m_Num1 - m_Num2; //怎么确定谁是减数,谁是被减数
}
else if (oper == "*")
{
return m_Num1 * m_Num2;
}
else if (oper == "/")
{
return m_Num1 / m_Num2; //怎么确定谁是除数,谁是被除数
}
else
{
return 0;
}
}
};
void test01()
{
Calculator cal;
cal.m_Num1 = 10;
cal.m_Num2 = 20;
cout << "两数相加:" << cal.getResult("+") << endl;
cout << "两数相减:" << cal.getResult("-") << endl;
}
int main()
{
test01();
}
多态方法实现
#include <iostream>
#include <string>
using namespace std;
class AbstractCalculator
{
public:
int m_Num1, m_Num2;
virtual int getResult()
{
return 0;
}
};
class AddCalculator:public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};
class SubtractCalculator:public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 - m_Num2;
}
};
class MultiplyCalculator:public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};
class DivideCalculator:public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 / m_Num2;
}
};
void test02()
{
AbstractCalculator* ac;
ac = new AddCalculator();
ac->m_Num1 = 20;
ac->m_Num2 = 10;
cout << ac->m_Num1 << " + " << ac->m_Num2 << " = " << ac->getResult() << endl;
// delete ac;
ac = new SubtractCalculator();
ac->m_Num1 = 20;
ac->m_Num2 = 10;
cout << ac->m_Num1 << " - " << ac->m_Num2 << " = " << ac->getResult() << endl;
// delete ac;
ac = new MultiplyCalculator();
ac->m_Num1 = 20;
ac->m_Num2 = 10;
cout << ac->m_Num1 << " * " << ac->m_Num2 << " = " << ac->getResult() << endl;
// delete ac;
ac = new DivideCalculator();
ac->m_Num1 = 20;
ac->m_Num2 = 10;
cout << ac->m_Num1 << " / " << ac->m_Num2 << " = " << ac->getResult() << endl;
}
int main()
{
test02();
}
/* 输出结果:
20 + 10 = 30
20 - 10 = 10
20 * 10 = 200
20 / 10 = 2 */
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现都是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual
返回值类型
函数名(参数列表) = 0;
当类中有个纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include <iostream>
using namespace std;
// 抽象类
class Base
{
public:
virtual void func() = 0;
};
class Son:public Base
{
public:
void func()
{
cout << "Son -> func()" << endl;
}
};
int main()
{
Son s;
}
多态案例 - 制作饮品
案例描述:
制作饮品的基本步骤1.煮水 2.冲泡 3.倒入杯中 4.加入辅料
使用多态方法实现,使用抽象类作为基类,制作咖啡和茶
咖啡步骤:1.煮水 2.冲泡咖啡 3.倒入杯中 4.加入糖和奶
茶步骤:1.煮水 2.冲泡茶叶 3.倒入杯中 4.加入柠檬
#include <iostream>
#include <string>
using namespace std;
class AbstractDrink
{
public:
//煮水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void AddMat() = 0;
void makeDrink()
{
Boil();
Brew();
PourInCup();
AddMat();
}
};
class MakeCoffee:public AbstractDrink
{
public:
void Boil()
{
cout << "1.煮水" << endl;
}
void Brew()
{
cout << "2.冲泡咖啡" << endl;
}
void PourInCup()
{
cout << "3.倒入杯中" << endl;
}
void AddMat()
{
cout << "4.加糖和牛奶" << endl;
}
};
class MakeTea:public AbstractDrink
{
public:
void Boil()
{
cout << "1.煮水" << endl;
}
void Brew()
{
cout << "2.冲泡茶叶" << endl;
}
void PourInCup()
{
cout << "3.倒入杯中" << endl;
}
void AddMat()
{
cout << "4.加柠檬" << endl;
}
};
void test01()
{
AbstractDrink *mk;
mk = new MakeCoffee();
mk->makeDrink();
// delete mk;
cout << "---------------" << endl;
mk = new MakeTea();
mk->makeDrink();
}
int main()
{
test01();
}
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构或纯虚析构。
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
- 纯虚析构需要声明,也需要实现
虚析构语法:virtual
~类名(){}
纯虚析构语法:
virtual
~类名()=0;
->声明类名::~类名(){}
-> 实现
如果子类中没有堆区数据,可以不写虚析构或纯虚析构
多态案例 - 电脑组装
案例描述:电脑主要组成部件有CPU、显卡、内存条;
将每个组件封装成抽象类,并提供不同的厂商生产不同的零件,例如:Intel、Lenovo;创建电脑类提供让电脑工作的函数,并调用每个零件工作的接口。测试时组装三台不同的电脑进行工作。
- 具体的代码实现待补充…
C++文件操作
程序运行时所产生的数据都是临时数据,程序一旦运行结束就会被释放。
通过文件可以将数据持久化。
C++中对文件操作需要包含头文件<fstream>
文件类型分两种:
- 文本文件 :文件以文本的ASCII码形式存储在计算机中
- 二进制文件 :文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类:
- ofstream : 写操作
- ifstream : 读操作
- fstream : 读写操作
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写入 |
ios::trunc | 如果文件存在先删除再创建 |
ios::binary | 二进制方式(二进制文件一定要指定此方式) |
Tips: 文件打开方式可以配合使用,使用“|”操作符。
文本文件 - 写文件
写文件的步骤:
- 包含头文件
<fstream>
- 创建流对象
- 指定打开方式和文件路径
- 写内容
- 关闭文件
#include <iostream>
#include <fstream> // 1.包含头文件
using namespace std;
void test01()
{
ofstream ofs; // 2.创建流对象
ofs.open("test.txt", ios::out); // 3.指定打开方式和文件路径
ofs<<"姓名:张三" << endl; // 4.写内容
ofs<<"性别:男" << endl;
ofs.close(); // 5.关闭文件
}
int main()
{
test01();
}
文本文件 - 读文件
读文件步骤:
- 包含头文件
<fstream>
- 创建流对象
- 打开文件并判断文件是否成功打开
- 读取数据 (有四种方式)
- 关闭文件
#include <iostream>
#include <fstream> // 1.包含头文件
#include <string>
using namespace std;
void wirteToFile()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs<<"姓名:张三" << endl;
ofs<<"性别:男" << endl;
ofs<<"年龄:28" << endl;
ofs.close();
}
void readDataFromFile()
{
ifstream ifs; // 2. 创建流对象
// 3. 打开文件并判断文件是否成功打开
ifs.open("test.txt", ios::in);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
}
/* 读取数据(有四种方式) */
// 第一种
char buf[1024] = {0};
while (ifs >> buf)
{
cout << buf << endl;
}
// 第二种
char buf[1024] = {0};
while (ifs.getline(buf, sizeof(buf)))
{
cout << buf << endl;
}
// 第三种
string buf;
while (getline(ifs, buf))
{
cout << buf << endl;
}
// 第四种 “不推荐此种方法”
char c;
while ((c = ifs.get()) != EOF) // EOF -> end of file
{
cout << c;
}
ifs.close();
}
int main()
{
wirteToFile();
readDataFromFile();
}
二进制文件 - 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是写的字节数
二进制文件 - 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读的字节数
#include <iostream>
#include <fstream> // 1.包含头文件
using namespace std;
class Person
{
public:
char m_Name[64];
int m_Age;
};
void binWrite()
{
ofstream ofs; // 2.创建流对象
ofs.open("person.txt", ios::out | ios::binary); // 3.打开文件
Person p={"李四", 28};
ofs.write((const char *) &p, sizeof(Person)); // 4.写入内容
ofs.close(); // 5.关闭文件
}
void binRead()
{
ifstream ifs; // 2.创建流对象
ifs.open("person.txt", ios::in | ios::binary); // 3.打开文件
if (! ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
Person pp;
ifs.read((char *)&pp, sizeof(Person)); // 4.读取内容
cout << "name: " << pp.m_Name << " age: " << pp.m_Age << endl;
ifs.close(); // 5.关闭文件
}
int main()
{
binWrite();
binRead();
}
练习项目
背景:
实现代码github地址:职工系统的代码实现