目录
一、内存分区模型
C++程序在运行时,将内存大方向划分为四个区域:
程序运行前:
代码区:存放函数体的二进制代码,由操作系统进行管理。
全局区:存放全局变量、静态变量,以及常量(字符串常量和const修饰的全局变量)。
程序运行后:
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
内存四区的意义:
不同的区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
1、程序运行前
在程序编译后,生成了exe可执行程序,在还没有执行这个程序之前,就有了两个区域:
代码区:存放CPU执行的机器指令(即写的代码)。
特点:共享:目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
只读:目的是防止程序意外的修改了它的指令。
全局区:存放全局变量,静态变量,常量区,常量区中包括字符串常量和const修饰的全局变量。
该区域的数据在程序结束后由操作系统释放。
#include<iostream>
using namespace std;
// 创建全局变量
int g_a = 10;
int g_b = 10;
/*创建常量:包括字符串常量和const修饰的变量,const修饰的变量包括:const修饰的全局变量和const修饰的局部变量。
注意字符串常量不能这么创建:string str_a = "hello world";这样的话就是变量了。*/
// 创建const修饰的全局变量(常量)
const int c_g_a = 10;
int main()
{
// 创建普通局部变量
int l_a = 10;
int l_b = 10;
// 创建静态变量:在普通变量前加static
static int s_a = 10;
static int s_b = 10;
// 创建const修饰的局部变量(常量)
const int c_l_b = 10;
cout << "局部变量l_a的地址为:" << (long long)&l_a << endl;
cout << "局部变量l_b的地址为:" << (long long)&l_b << endl;
cout << endl;
cout << "全局变量g_a的地址为:" << (long long)&g_a << endl;
cout << "全局变量g_b的地址为:" << (long long)&g_b << endl;
cout << endl;
cout << "静态变量s_a的地址为:" << (long long)&s_a << endl;
cout << "静态变量s_b的地址为:" << (long long)&s_b << endl;cout << endl;
cout << "字符串常量的地址为:" << (long long)&"hello world" << endl;
cout << "const修饰的全局变量(常量)c_g_a的地址为:" << (long long)&c_g_a << endl;
cout << "const修饰的局部变量(常量)c_l_b的地址为:" << (long long)&c_l_b << endl;
return 0;
}
可以发现,常量中的字符串常量和const修饰的全局常量,与全局、静态这些数据都很近,它们都放在全局区,但局部变量地址就和它们差很远,不在一个区。
特殊的,如上红框中的,要注意,只要是局部的,存放的区域都不在全局区。
2、程序运行后
栈区和堆区。
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。
注:不要返回局部变量的地址!因为局部变量是放在栈区的,而栈区的数据在函数执行完后会自动释放,执行完函数后是拿不到局部变量的。
#include<iostream>
using namespace std;
int * func()
{
int a = 10;
// 返回局部变量的地址
return &a;
}
int main()
{
int * p = func();
cout << *p << endl;
return 0;
}
vs code 会报错:
因为不能在函数中返回一个局部变量的地址。
在vs中可以临时打印输出一次,是为了防止误操作,不过还是要记住,不要在函数中返回局部变量的地址。
堆区:由程序员分配释放,若程序员不释放,程序结束后由操作系统回收。
在堆区开辟内存:new 数据类型(数据值)
创建这个内存,它会返回这个地址的编号(指针),而不是值:如果cout << new int(10) << endl;它的输出结果为十六进制地址(指针)编号:0x6e17f0
#include<iostream>
using namespace std;
int * func()
{
// 利用new关键字开辟堆区内存,用指针p存放它的地址,注意指针本质还是局部变量,它是存放在栈区的,但指针保存的这个数据是存放在堆区的。
int * p = new int(10);
// 返回堆区的地址
return p;
}
int main()
{
int * p = func();
cout << *p << endl;
// 输出的就是堆区存的数据10
return 0;
}
我们只是把堆区的数据的地址编号,用一个栈区的数据保存住了,当我解引用后,拿到的就是我们保存在堆区的数据10。
在堆区释放内存:delete 变量名;
delet[] 数组名;
#include<iostream>
using namespace std;
int * func()
{
// 利用new关键字开辟堆区内存。
int * p = new int(10);
return p;
}
int main()
{
int * p = func();
cout << *p << endl;
// 输出的就是堆区存的数据10,这个数据不会被释放,它由程序员管理开辟,管理释放。
delete p;
cout << *p << endl;
// 这个就会报错,因为这块内存已经被释放了。
return 0;
}
#include<iostream>
using namespace std;
// 利用new创建数组:
void * func()
{
// 在堆区创建一个有10个整型数据的数组
int * arr = new int[10];
for(int i=0;i<10;i++)
{
// 给这10个数据赋值:100-109
arr[i] = i+100;
}
// 输出这个数组
for(int i=0;i<10;i++)
{
cout << arr[i] << endl;
}
// 释放数组
delete[] arr;
}
int main()
{
func();
return 0;
}
二、引用
1、引用的基本语法与注意事项
数据类型 &别名 = 原名;
引用的作用就是给变量起别名。
如果把b改成20了,那么a也会变成20,对引用的操作都会转为对引用对象的操作。
引用必须要初始化。
引用初始化后,就不可以更改。
2、引用与函数
1、引用做函数参数:函数传参时,可以利用引用的技术让形参修改实参,这样就可以不用指针了。
函数可以有三种传参方式:值传递、地址传递、引用传递。
#include<iostream>
using namespace std;
// 引用传递
void swap(int &c,int &d)
{
// 这里的c就相当于下面的a,d就相当于b
int temp = c;
c = d;
d = temp;
}
int main()
{
int a = 10;
int b = 20;
swap(a,b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
// 输出:a=20 b=10
return 0;
}
2、引用做函数返回值,注意不要返回局部变量的引用。
不要返回局部变量的引用:
#include<iostream>
using namespace std;
int& test01()
{
// 局部变量,存放在栈区
int a = 10;
return a;
}
int main()
{
int & ret = test01();
// 相当于是 int & ret = a,即给a的别名赋值为ret,ret就是a的别名。
// 这个会报错!
return 0;
}
返回的a是以类型“int&”返回的,所以必须要以类型“int&”去接收,这里用ret去接收,即int& ret = a ,即ret为a的别名。
函数的调用可以作为左值:
#include<iostream>
using namespace std;
int& test02()
{
// 静态变量,存放在全局区,全局区上的数据在程序结束后系统释放。
static int a = 10;
return a;
}
int main()
{
int &ret = test02();
cout << "ret=" << ret << endl;
// 函数调用作为左值:
test02() = 1000;
cout << "ret=" << ret << endl;
// 输出:ret=10 ret=1000
return 0;
}
test02()调用完后返回的是a的引用,只是现在还没有给它赋值,test02()=1000,就是a的引用ret=1000,相当于就是对a进行操作。
3、引用的本质
引用的本质在C++内部实现是一个指针常量。
指针常量:指针指向不能修改,指针指向的值可以修改。
#include <iostream>
using namespace std;
// 发现是引用,转换为int * const ret = &a;
void func(int &ret)
{
ret = 100;
}
int main()
{
int a = 10;
// 自动转换为int * const ret = &a;指针常量,指针指向不可修改,也就说明为什么引用不可更改。
int &ret = a;
ret = 20;
cout << a << endl;
cout << ret << endl;
func(a);
cout << a << endl;
cout << ret << endl;
// 输出:20 20 100 100
return 0;
}
4、常量引用
常量引用主要用来修饰形参,防止形参改变实参。
const int &ret = 10;
加上const之后,编译器将代码修改为int temp=10; const int &ret =temp;
也就是说编译器自己先给你的10起了个变量名,但是你不知道这个变量名没办法用它,你只能用你自己给它赋的别名ret来使用它。
#include<iostream>
using namespace std;
void print(const int &val)
{
// val = 1000;
// 这时候就不能修改这个val了,const的作用就是这个
cout << "val=" << val << endl;
}
int main()
{
int a = 10;
print(a);
cout << "a=" << a << endl;
// 输出:val=10 a=10
return 0;
}
三、函数提高
1、函数参数
函数默认参数:返回值类型 函数名 (参数=默认值) {}
函数占位参数:返回值类型 函数名 (数据类型) {}
假如写的是int,调用的时候就必须传入一个整数才行。(仅演示占位参数)
#include<iostream>
using namespace std;
void func(int a,int)
{
cout << "函数func()调用..." << endl;
}
int main()
{
func(10,20);
// 实际上传的这个第二个数,也就是20,在函数体中是用不到的,所以用到的比较少。
return 0;
}
占位参数也可以有默认参数。
#include<iostream>
using namespace std;
void func(int a,int = 20)
{
cout << "函数func()调用..." << endl;
}
int main()
{
func(10);
// 给占位参数设置默认参数后,就不需要给它传值了。
return 0;
}
2、函数重载
作用:在C++中,函数名是可以相同的,提高复用性。
函数重载条件:同一个作用域下(比如,都是全局函数)。
函数参数类型不同,或者个数不同,或者顺序不同。
#include<iostream>
using namespace std;
void func()
{
cout << "func的调用..." << endl;
}
void func(int a)
{
cout << "func的调用。。。" << endl;
}
int main()
{
func();
func(10);
return 0;
}
注:函数返回值不可以作为函数重载的条件,比如void func()和int func(),他们俩不能做函数重载。
函数重载需要注意:
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(int &a)的调用,因为const是只读不可写的,而当我们传入可读可写的变量a,一般来说,会倾向于调用那个可读可写的函数。
func(10);
// 输出结果为:func(const int &a)的调用,因为第一个函数会变成int &a=10;这是不合法的,第二个就会是:const int &a=10;这是合法的。
return 0;
}
2、函数重载遇到默认参数时,就会出现歧义,会报错,这要尽量避免这种情况。
#include<iostream>
using namespace std;
void func(int a,int b=10)
{
cout << "func(int a,int b=10)的调用" << endl;
}
void func(int a)
{
cout << "func(int a)的调用" << endl;
}
int main()
{
func(10);
// 这个会报错!
return 0;
}
四、类和对象
C++认为,万事万物皆对象,对象上有其属性和行为。
比如,车作为对象,属性有轮胎、方向盘、车灯,行为有载人、放音乐、开空调等。
具有相同性质的对象,我们可以抽象为类,比如,人属于人类,车属于车类。
1、封装
class 类名 {访问权限: 属性 行为};
属性通常是变量,行为通常是函数。
通过一个类创建一个对象的过程就是实例化。
(1)封装的意义
将属性和行为作为一个整体,表现生活中的事物。将属性和行为加以权限控制。
案例:设计一个计算圆周长的类。
#include<iostream>
using namespace std;
const double PI = 3.14;
class Circle
{
// 访问权限:公共权限
public:
// 属性:半径,一般属性用变量来表示。
int r;
// 行为:计算周长,一般行为用函数来表示。
double calculate_c()
{
return 2*PI*r;
}
};
int main()
{
// 通过圆类创建具体的圆(对象):
Circle c1;
// 给圆对象的属性进行赋值:
c1.r = 10;
cout << "圆的周长为:" << c1.calculate_c() << endl;
return 0;
}
案例:学生类,有姓名、学号等信息,并且可以打印出来。
#include<iostream>
using namespace std;
class Student
{
public:
string name;
long long id;
void print()
{
cout << "姓名:" << name << endl;
cout << "学号:" << id << endl;
}
};
int main()
{
Student s1;
s1.name = "小明";
s1.id = 123456;
Student s2;
s2.name = "小红";
s2.id = 456789;
s1.print();
s2.print();
return 0;
}
(2)访问权限
public(公共权限)、protected(保护权限)、private(私有权限)。
公共权限是成员 类内可以访问,类外也可以访问。
保护权限是成员 类内可以访问,类内不可以访问。
私有权限是成员 类内可以访问,类内不可以访问。
后两个在继承的时候体现,父类中的保护权限内容,子类也可以访问。子类不可访问父类的私有权限内容。(想让孩子拿到——保护,不想让孩子拿到——私有)
#include <iostream>
using namespace std;
class Person
{
public:
string name;
protected:
string car;
private:
long long password;
public:
// 在类内部,这个函数体都能访问到
void func()
{
name = "张三";
car = "拖拉机";
password = 123456;
}
};
int main()
{
Person p1;
p1.name = "李四"; // 这个可以访问
p1.car = "奔驰"; // 这个不能访问
p1.password = 654321; // 这个不能访问
return 0;
}
(3)struct和class的区别
在C++中差不太多,唯一的区别是,struct默认权限为公共,class默认权限为私有。
#include <iostream>
using namespace std;
class C
{
// 默认权限是私有
int a;
};
struct S
{
// 默认权限是公共
int b;
};
int main()
{
C c1;
c1.a = 100; // 这个会报错
S s1;
s1.b = 100; // 这个不会
return 0;
}
(4)成员属性私有化
通常会将成员属性设为私有,因为:
1、可以自己控制读写权限;2、对于写的权限(如修改数据),可以监测数据的有效性。
#include <iostream>
using namespace std;
class Person
{
public:
// 提供公共接口来访问私有属性
// 可读可写的name-可写:
void setname(string name)
{
m_name = name;
}
// 可读可写的name-可读:
string getname()
{
return m_name;
}
// 只读的m_age:
int getage()
{
// 初始化年龄
m_age = 10;
return m_age;
}
// 只写的m_lover:
void setlover(string lover)
{
m_lover = lover;
}
// 通常,我们把一些属性设置为私有,会再提供public接口来对这些属性进行访问(见上)
private:
// 可读可写
string m_name;
// 只读
int m_age;
// 只写
string m_lover;
};
int main()
{
Person p;
// 调用m_name的公共接口
p.setname("张三");
cout << "姓名为:" << p.getname() << endl;
// 调用m_age的公共接口
cout << "年龄为:" << p.getage() << endl;
// 调用m_lover的公共接口
p.setlover("一堆帅哥");
return 0;
}
(5)案例练习
练习1:设计立方体类,求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
#include <iostream>
using namespace std;
class Cube
{
public:
// 设置、获取:长
void setm_L(int L)
{
m_L = L;
}
int getm_L()
{
return m_L;
}
// 设置、获取:宽
void setm_W(int W)
{
m_W = W;
}
int getm_W()
{
return m_W;
}
// 设置、获取:高
void setm_H(int H)
{
m_H = H;
}
int getm_H()
{
return m_H;
}
// 计算立方体面积
int CalculateS()
{
return 2*m_L*m_W + 2*m_W*m_H + 2*m_L*m_H;
}
// 计算立方体体积
int CalculateV()
{
return m_L*m_H*m_W;
}
// 利用成员函数判断两个立方体是否相等
bool samebyclass(Cube &c)
{
// 自身和传进来的那个进行判断
if(m_L == c.getm_L() && m_W == c.getm_W() && m_H == c.getm_H())
{
return true;
}
return false;
}
// 属性:长宽高
private:
int m_L;
int m_W;
int m_H;
};
// 利用全局函数判断两个立方体是否相等。利用引用的方式传入数据,这样就不会多余拷贝一份数据出来。
bool samebyall(Cube &c1,Cube &c2)
{
if(c1.getm_L() == c2.getm_L() && c1.getm_W() == c2.getm_W() && c1.getm_H() == c2.getm_H())
{
return true;
}
return false;
}
int main()
{
// 创建立方体对象
Cube c1;
c1.setm_L(10);
c1.setm_W(10);
c1.setm_H(10);
cout << "c1的长为:" << c1.getm_L() << endl;
cout << "c1的宽为:" << c1.getm_W() << endl;
cout << "c1的高为:" << c1.getm_H() << endl;
cout << "c1的面积为:" << c1.CalculateS() << endl;
cout << "c1的体积为:" << c1.CalculateV() << endl;
// 创建第二个立方体对象,分别利用全局函数和成员函数判断两个立方体是否相等。
Cube c2;
c2.setm_L(10);
c2.setm_W(10);
c2.setm_H(10);
// 利用全局函数判断两个立方体是否相等:
bool ret1 = samebyall(c1,c2);
if(ret1)
{
cout << "全局函数判断:c1和c2相等" << endl;
}
else
{
cout << "全局函数判断:c1和c不2相等" << endl;
}
// 利用成员函数判断两个立方体是否相等:
bool ret2 = c1.samebyclass(c2);
if(ret2)
{
cout << "成员函数判断:c1和c2相等" << endl;
}
else
{
cout << "成员函数判断:c1和c不2相等" << endl;
}
return 0;
}
练习2:设计一个圆类,和一个点类,计算点和圆的关系。
#include <iostream>
using namespace std;
// 点类
class Point
{
public:
// 设置和获取x、y的公共接口
void setx(int x)
{
m_x = x;
}
int getx()
{
return m_x;
}
void sety(int y)
{
m_y = y;
}
int gety()
{
return m_y;
}
// 属性:x坐标,y坐标
private:
int m_x;
int m_y;
};
// 圆类
class Circle
{
public:
// 设置和获取半径、圆心
void setr(int r)
{
m_r = r;
}
int getr()
{
return m_r;
}
void setcenter(Point center)
{
m_center = center;
}
Point getcenter()
{
return m_center;
}
// 属性:半径、圆心
int m_r;
// 在一个类中可以让另一个类作为本类成员
Point m_center;
};
// 判断点和圆的关系的函数
void Judge(Circle &c,Point &p)
{
// 计算两个点之间的距离平方
int distance = (c.getcenter().getx() - p.getx())*(c.getcenter().getx() - p.getx()) + (c.getcenter().gety())*(c.getcenter().gety());
// 计算半径平方
int rr = c.getr()*c.getr();
// 判断关系
if(distance == rr)
{
cout << "点在圆上" << endl;
}
else if(distance >= rr)
{
cout << "点在圆外" << endl;
}
else
{
cout << "点在圆内" << endl;
}
}
int main()
{
// 创建圆
Circle c;
c.setr(10);
// 创建圆心
Point center;
c.setcenter(center);
center.setx(10);
center.sety(0);
// 创建点
Point p;
p.setx(10);
p.sety(10);
// 判断关系
Judge(c,p);
return 0;
}
2、对象特性
也叫对象的初始化和清理:就像生活中我们买手机会有出厂设置,不要了的手机我们会彻底清理数据一样,C++的每个对象也都会有初始化以及清理数据的设置。
(1)构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题,C++中利用的是构造函数和析构函数来解决上面的问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作。
构造函数:类名(){}
没有返回值也不写void;
函数名与类名相同;
构造函数可以有参数,因此可以发生重载;
程序在调用的对象的时候会自动调用它,无需手动调用,且只会调用一次。
主要作用在于,在创建对象时为对象的成员属性赋值。
析构函数:~类名(){}
没有返回值也不写void;
函数名与类名相同,在名称前加~;
析构函数不可以有参数,因此不可以发生重载;
程序在调用的对象的时候会自动调用它,无需手动调用,且只会调用一次。
主要作用在于,在对象销毁前,系统自动调用,执行一些清理工作。
#include <iostream>
using namespace std;
class Person
{
public:
// 构造函数
Person()
{
cout << "Person构造函数调用" << endl;
}
// 析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
void test()
{
Person p1;
}
int main()
{
// 只创建p1和p2就能直接调用构造函数和析构函数。
// 在test函数中调用,构造和析构都会执行
test();
// 在main函数中直接调用,只有构造函数会执行
Person p2;
return 0;
}
是什么导致了这个不同?
如果在test()里面,属于局部变量,存放在堆区,调用完之后就会释放,所以在释放之前,会调用析构函数,但如果在main函数中就不会被释放,除非使把整个main函数都执行完了,也就是执行了system ("pause");之后,return 0;之前,它才会执行这个析构函数,只不过因为执行完了之后窗口会关闭,它的执行过程我们看不到。
(2)构造函数的分类和调用
分类:按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造
三种调用方式:括号法、显示法、隐式转换法
拷贝构造:类名(const 类名 &变量名)
const:不允许修改拷贝进来的那份数据。
#include <iostream>
using namespace std;
class Person
{
public:
// 一、构造函数的分类:
// 1、按照参数分类:无参构造(默认构造)
Person()
{
cout << "Person的无参构造函数调用" << endl;
}
// 1、按照参数分类:有参构造
Person(int a)
{
age = a;
cout << "Person的有参构造函数调用" << endl;
}
// 2、按照类型分类:普通构造(以上都是)、拷贝构造(注意不能把传进来的那个改了,所以要const,而且得以引用的方式传进来,就相当于是给它起个别名,两个人不能完全一样)
// 这里演示传入有参构造函数
Person(const Person &p)
{
// 将传入的人身上的所有属性拷贝到我的身上,谁调用这个拷贝函数,把属性拷贝到谁身上
age = p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
int age;
};
int main()
{
// 二、构造函数的调用
// 1、括号法
// 默认构造函数的调用:注意不要加小括号像这样Person a1(),这不会运行。是因为编译器会认为是一个函数的声明,跟void test()特别像,所以不会认为在创建对象。
Person a1;
// 有参构造函数的调用
Person a2(10);
// 拷贝构造函数的调用
Person a3(a2);
// 关于拷贝构造函数的用途
cout << "a2的年龄为:" << a2.age << endl;
cout << "a3的年龄为:" << a3.age << endl;
cout << endl;
// 2、显示法
// 默认构造函数的调用
Person b1;
// 有参构造函数的调用
Person b2 = Person(10);
// 拷贝构造函数的调用
Person b3 = Person(b2);
// 如果单独拿出Person(10),这叫匿名对象,特点:这句话执行结束后,系统会立即回收掉它。
Person(10);
// 注意不要在调用匿名函数的时候选择匿名对象的方式,或者说不要在初始化匿名对象的时候利用拷贝函数来构造,下面这句就会报错,编译器会认为Person(b3)等价于Person b3,而上面我们已经定义过b3了(Person b3 = Person(b2);)这就重定义了。
Person(b3);
cout << endl;
// 3、隐式转换法
// 有参构造函数调用:下面这句相当于Person p4 = Person(10)
Person c1 = 10;
// 拷贝构造函数调用
Person c2 = c1;
return 0;
}
(3)触发拷贝构造函数的调用
C++中触发拷贝构造函数的调用通常有三种情况:
1、使用一个已经创建完毕的对象来初始化一个新对象(已经有一个了,克隆一个新的);
2、值传递的方式给函数参数传值;
3、以值方式返回局部对象。
粗浅的理解:只要在过程中创建一个临时的新副本,就会触发拷贝函数。
#include <iostream>
using namespace std;
class Person
{
public:
// 无参构造(默认构造)
Person()
{
cout << "Person的无参构造函数调用" << endl;
}
// 有参构造
Person(int a)
{
age = a;
cout << "Person的有参构造函数调用" << endl;
}
// 拷贝构造
Person(const Person &p)
{
age = p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
// 析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
public:
int age;
};
// 1、使用一个已经创建完毕的对象来初始化一个新对象(已经有一个了,克隆一个新的)
void test01()
{
Person p1(20);
Person p2(p1);
cout << "p2的年龄为:" << p2.age << endl;
}
// 2、值传递的方式给函数参数传值
void deliverwork(Person p3){}
void test02()
{
Person p3;
// 在实参传给形参的时候,会调用拷贝构造函数,这个p3在传入上面那个deliverwork()函数后,会拷贝出一个新的数据p3。
deliverwork(p3);
}
// 3、值方式返回局部对象,当函数的返回值是类对象时,系统自动调用拷贝构造函数。(注意会有编译器可能会进行优化,而观察不到拷贝的发生)
Person returnwork()
{
Person p4;
// return的时候不会返回p4,它会按照p4拷贝一个新的出来,返回给外边。
return p4;
}
void test03()
{
Person p = returnwork();
}
int main()
{
test01();
cout << endl;
test02();
cout << endl;
test03();
return 0;
}
(4)构造函数的调用规则
默认情况下C++编译器至少给一个类添加3个函数:
默认构造函数(无参,函数体为空)、默认析构函数(无参,函数体为空)、默认拷贝构造函数,对属性进行值拷贝。
构造函数的调用规则如下:
如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造。
如果用户定义拷贝构造函数,C++不再提供其他构造函数。
1、编译器会默认给用户提供3个函数:
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person的默认构造函数调用" << endl;
}
Person(int age)
{
myage = age;
cout << "Person的有参构造函数调用" << endl;
}
Person(const Person & p)
{
myage = p.myage;
cout << "Person的拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
public:
int myage;
};
void test01()
{
Person p1;
p1.myage = 18;
Person p2(p1);
cout << "p2的年龄为:" << p2.myage << endl;
}
int main()
{
test01();
return 0;
}
如果注释掉拷贝构造函数:
再运行程序,myage结果依然不变:
其实是编译器默认提供了拷贝构造函数。
2、如果写了有参函数,编译器就不再提供默认构造函数,但依然提供拷贝构造。
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
myage = age;
cout << "Person的有参构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
public:
int myage;
};
void test02()
{
// Person p; 这行代码会报错,因为没有合适的默认构造函数可用。
// 调用有参构造函数和拷贝构造函数:
Person p1(22);
Person p2(p1);
cout << "p2的年龄为:" << p2.myage << endl;
}
int main()
{
test02();
return 0;
}
3、如果写了拷贝构造函数,编译器不再提供其他构造函数。
#include <iostream>
using namespace std;
class Person
{
public:
Person(const Person & p)
{
myage = p.myage;
cout << "Person的拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
public:
int myage;
};
void test03()
{
// Person p; 这行代码会报错,因为没有合适的默认构造函数可用。
// Person p1(18); 这行代码也会报错,因为没有合适的有参构造函数可用,当然本身它也不提供有参构造函数。
// 只有拷贝构造函数时,可以这么调用:
Person p = Person(p);
p.myage = 26;
cout << "p的年龄为:" << p.myage << endl;
}
int main()
{
test03();
return 0;
}
(5)浅拷贝与深拷贝
浅拷贝:简单的赋值拷贝操作。
深拷贝:在堆区重新申请空间,进行拷贝操作。
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person的默认构造函数调用" << endl;
}
Person(int age,int height)
{
myage = age;
myheight = new int(height);
cout << "Person的有参构造函数调用" << endl;
}
Person(const Person & p)
{
// 下面两行就是编译器默认实现的拷贝构造函数的代码(浅拷贝)
myage = p.myage;
myheight = p.myheight;
cout << "Person的拷贝构造函数调用" << endl;
}
// 析构代码可以帮助将我们在堆区开辟的数据释放
~Person()
{
if(myheight != NULL)
{
delete myheight;
// 防止野指针出现:
myheight = NULL;
}
cout << "Person的析构函数调用" << endl;
}
public:
int myage;
// 用指针类型的数据来定义身高。
int * myheight;
};
void test01()
{
Person p1(18,160);
cout << "p1的年龄为:" << p1.myage << ",身高为:" << *p1.myheight << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.myage << ",身高为:" << *p2.myheight << endl;
}
int main()
{
test01();
return 0;
}
浅拷贝的问题,需要深拷贝来解决。
自己写一个拷贝构造函数,在堆区再开一块内存存上160:
(6)初始化列表
C++提供了初始化列表语法,用来初始化属性。
构造函数(): 属性1(值1),属性2(值2)...{}
传统初始化属性操作:
#include <iostream>
using namespace std;
class Person
{
public:
// 传统初始化操作:
Person(int a,int b,int c)
{
m_a = a;
m_b = b;
m_c = c;
}
int m_a;
int m_b;
int m_c;
};
void test01()
{
Person p1(10,20,30);
cout << p1.m_a << endl;
cout << p1.m_b << endl;
cout << p1.m_c << endl;
}
int main()
{
test01();
return 0;
}
列表初始化属性操作:
#include <iostream>
using namespace std;
class Person
{
public:
// 列表初始化属性操作:
Person():m_a(10),m_b(20),m_c(30){}
public:
int m_a;
int m_b;
int m_c;
};
void test02()
{
Person p2;
cout << p2.m_a << endl;
cout << p2.m_b << endl;
cout << p2.m_c << endl;
}
int main()
{
test02();
return 0;
}
更灵活的传入:
调用的时候只需要:
(7)类对象作为类成员
C++中一个类的成员可以是另一个类的对象,我们称该成员为对象成员。
B类中有对象A作为成员,A为对象成员。
#include <iostream>
using namespace std;
// 手机类
class Phone
{
public:
Phone(string pname)
{
phonename = pname;
cout << "Phone的构造函数调用" << endl;
}
~Phone()
{
cout << "Phone的析构函数调用" << endl;
}
// 手机属性:品牌名、
string phonename;
};
// 人类
class Person
{
public:
Person(string name,string pname):myname(name),myphone(pname)
{
cout << "Person的构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
// 属性:姓名、手机
string myname;
Phone myphone;
};
void test01()
{
Person p("张三","苹果MAX");
cout << "姓名:" << p.myname << endl;
cout << "手机型号:" << p.myphone.phonename << endl;
}
int main()
{
test01();
return 0;
}
当其他类的对象作为本类的成员,构造的时候会先构造其他类的对象,再构造自身。析构的顺序则与构造相反。
(8)静态成员
静态成员就是在成员变量和成员函数前加关键字static,称为静态成员。
静态成员分为静态成员变量和静态成员函数。
1、静态成员变量
静态成员变量:1、所有对象共享同一份数据;2、在编译阶段分配内存;3、类内声明,类外初始化。
#include <iostream>
using namespace std;
class Person
{
public:
// 类内声明
static int m_a;
};
// 类外初始化
int Person:: m_a = 100;
void test01()
{
Person p1;
cout << p1.m_a << endl;
// p2更改了值以后,p1p2的值都会改变,因为共享一份数据
Person p2;
p2.m_a = 200;
cout << p1.m_a << endl;
cout << p2.m_a << endl;
}
int main()
{
test01();
return 0;
}
静态成员变量不属于某个对象,所有对象共享同一份数据,因此静态成员变量有两种访问方式:1、通过对象进行访问;2、通过类名进行访问。
#include <iostream>
using namespace std;
class Person
{
public:
static int m_a;
};
int Person:: m_a = 100;
void test02()
{
// 1、通过对象进行访问
Person p1;
cout << p1.m_a << endl;
// 2、通过类名进行访问
cout << Person::m_a << endl;
}
int main()
{
test02();
return 0;
}
静态成员变量也是有访问权限的:下面的会报错,类外访问不到这个私有变量。
#include <iostream>
using namespace std;
class Person
{
private:
static int m_b;
};
int Person:: m_b = 200;
void test03()
{
cout << Person::m_b << endl;
}
int main()
{
test03();
return 0;
}
2、静态成员函数
静态成员函数:1、所有对象共享同一个函数;2、静态成员函数只能访问静态成员变量。
访问静态成员函数的两种方法:1、通过对象访问;2、通过类名访问。
#include <iostream>
using namespace std;
class Person
{
public:
static void func()
{
cout << "static void func()的调用" << endl;
}
};
void test01()
{
// 1、通过对象访问
Person p;
p.func();
// 2、通过类名访问
Person::func();
}
int main()
{
test01();
return 0;
}
静态成员函数只能访问静态成员变量:
#include <iostream>
using namespace std;
class Person
{
public:
static int m_a;
int m_b;
static void func()
{
// 静态成员函数可以访问静态成员变量:
m_a = 100;
// 但不能访问非静态成员变量,下面这句会报错:
m_b = 200;
}
};
int Person::m_a = 0;
int main()
{
return 0;
}
这是因为m_b必须通过对象进行访问,只有创建对象,才能去读写m_b的数据,当调用静态成员函数func()时,这个函数体内部不知道改变的是哪一个对象上的m_b,这里没有创建对象,所以也根本不能访问。而m_a是大家共享的,只有一份,当然可以改。(m_a只有一份,m_b可以有很多)
同样,私有作用域下也访问不到静态成员函数。
(9)成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态的成员变量才属于类的对象。
空对象占用的内存空间为:1
#include <iostream>
using namespace std;
class Person{};
void test01()
{
Person p;
cout << "空对象占用的内存空间为:" << sizeof(p) << endl;
}
int main()
{
test01();
return 0;
}
C++编译器会给每个空对象也分配一个字节空间,是为了区分不同的空对象占内存的位置。
当初始化一个属性后:
#include <iostream>
using namespace std;
class Person
{
public:
int m_a;
};
void test02()
{
Person p;
cout << "初始化一个属性后占用的内存空间为:" << sizeof(p) << endl;
}
int main()
{
test02();
return 0;
}
如果是空对象,那么默认分配一个内存,如果里面有属性,就按照它们的类型来分配,比如上面的int m_a,是int型,就分配了4字节。
当有静态成员变量后:
#include <iostream>
using namespace std;
class Person
{
public:
int m_a;
static int m_b;
};
int Person::m_b = 0;
void test03()
{
Person p;
cout << "有静态成员变量后,占用的内存空间为:" << sizeof(p) << endl;
}
int main()
{
test03();
return 0;
}
是因为静态成员变量不属于某个对象上。
当有成员函数后:
#include <iostream>
using namespace std;
class Person
{
public:
int m_a;
void func(){}
};
void test04()
{
Person p;
cout << "有成员函数后,占用的内存空间为:" << sizeof(p) << endl;
}
int main()
{
test04();
return 0;
}
这是因为,成员变量和成员函数是分开存储的,非静态的成员变量,它是属于类的对象上的,可以复制无数份,但是非静态成员函数却不属于某个类的对象,它只有一份。
(10)this指针
上面提到,C++成员变量和成员函数是分开存储的,函数只有一份,那么当多个对象同时调用这个函数时,它如何区分是哪个对象调用它呢?
C++通过提供this指针解决上述问题,this指针指向被调用的成员函数所属对象(指向调用函数的那个对象)。
比如说,p1调用这个函数了,this就指向p1,p2调用了这个函数,this就指向p2。
this指针隐含在每一个非静态成员函数内,不需要定义,直接使用即可。
用途是:1、当形参和成员变量同名时,可以用this指针来区分;
2、在类的非静态成员函数中返回对象本身,可使用return *this。
先来看1,解决名称冲突:
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
age = age;
}
int age;
};
void test01()
{
Person p(18);
cout << p.age << endl; // 输出的是奇怪数据
}
int main()
{
test01();
return 0;
}
编译器会认为构造函数中的3个age是同一份,它们跟属性age不是同一个,它认为你没有给属性age赋值,所以输出的是奇怪数据。
方法一:做一个区分,成员属性和我们传入的形参的名称,要有一个规范:
方法二:使用this指针:
编译器就会把这3个age视为同一个age,this指针指向的是调用成员函数的那个对象,也就是p,所以this->age,就相当于p.age。
2,返回对象本身用*this:
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
this->age = age;
}
Person& Addage(Person &p)
{
// 把传进来的对象的年龄加到自身上面:
this->age += p.age;
// 返回值设置成这个对象自身,则外部可以一直调用这个函数:
return *this;
}
int age;
};
void test02()
{
Person p1(10);
Person p2(8);
// 链式编程思想:
p2.Addage(p1).Addage(p1);
cout << "p2的年龄为:" << p2.age << endl;
}
int main()
{
test02();
return 0;
}
*this解引用出来是p2,而必须再套一层引用(指针)才能指向p2(对p2修改)
即必须要指针的指针才能对p2修改,否则return的只是p2的副本(传值)
如果代码改成:Person Addage(Person &p),结果就会输出为:p2的年龄为18
这是值的方式在返回,当调用完第一次后,p2加了10岁了,返回的就不是p2的本体了,它是调用了拷贝构造函数,创建了一个新的数据,每次调用都是一个新的对象,不是在它自身身上改数据,当cout << p2.age时,输出的只是第一次调用Addage()的那个数据,也就是18。
(11)空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
若用到this指针,需要加以判断保证代码的健壮性。
#include <iostream>
using namespace std;
class Person
{
public:
void showclassname()
{
cout << "类名:Person" << endl;
}
void showage()
{
cout << "age = " << this->my_age << endl;
}
int my_age;
};
void test01()
{
Person *p = NULL;
p->showclassname();
p->showage();
// 第二句会报错,第二句函数会调用my_age这个属性,因为传入的指针为空,也就相当于没创建对象,访问里面的属性肯定是报错的。
// 成员函数不属于某个对象,成员属性属于。
}
int main()
{
test01();
return 0;
}
为了避免这种情况的发生,需要:
这样会提高代码的健壮性。
(12)const修饰成员函数
常函数:
1、成员函数后加const后,我们称之为常函数;
2、常函数内不可以修改成员属性;
3、成员属性声明时加关键字mutable后,在常函数中依然可以修改。
常对象:
1、声明对象前加const称该对象为常对象;
2、常对象必须初始化,需要手动书写默认构造函数才能创建常对象;
3、常对象只能调用常函数。
通常,我们是修改成员属性的:
如果我们不想修改,则:
原理:事实上,隐含在每个成员函数内部,都有一个this指针,this指针的本质是一个指针常量,指针的指向是不可以修改的,它实际上是:Person * const this,而当我们前面再加一个const,就变成了:const Person * const this,就相当于不仅指针的指向不能修改了,指针指向的值也不能修改了。
如果我们一定想在常函数内部修改成员属性的值:
关于常对象:
#include <iostream>
using namespace std;
class Person
{
public:
// 常对象必须初始化,必须显式书写出来默认构造函数:
Person()
{
my_A = 100;
}
void showperson() const
{
my_B = 100;
}
void func()
{
my_A = 100;
}
int my_A;
mutable int my_B;
};
void test01()
{
const Person p;
p.my_A = 200; // 这句会报错
p.my_B = 200;
p.showperson();
p.func(); // 这句会报错,常对象不能调用普通成员函数,因为普通成员函数颗以修改成员属性,而常对象是不可以的。
}
int main()
{
test01();
return 0;
}
3、友元
生活中你的家有客厅(Public),有你的卧室(Private)。所有来的客人都可以进客厅,但是你的卧室是私有的,只有你能进去,但是你也可以允许你的好朋友进去。
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术:友元的目的就是让一个函数或者类访问另一个类中私有成员。
友元关键字:friend
友元的三种实现:全局函数做友元、类做友元、成员函数做友元
(1)全局函数做友元
#include <iostream>
using namespace std;
class Home
{
// 声明友元,Myfriend()全局函数是Home的好朋友,可以访问Home中的私有成员。
friend void Myfriend(Home *home);
public:
Home()
{
sittingroom = "客厅";
bedroom = "卧室";
}
public:
string sittingroom;
private:
string bedroom;
};
// 全局函数:传入一个对象
void Myfriend(Home *home)
{
// 访问公共属性
cout << "全局函数好朋友正在访问" << home->sittingroom << endl;
// 访问私有属性
cout << "全局函数好朋友正在访问" << home->bedroom << endl;
}
int main()
{
Home home;
Myfriend(&home);
return 0;
}
(2)类做友元
#include <iostream>
using namespace std;
class Home
{
// 声明好朋友类是友元
friend class Myfriend;
public:
Home()
{
sittingroom = "客厅";
bedroom = "卧室";
}
public:
string sittingroom;
private:
string bedroom;
};
class Myfriend
{
public:
Myfriend()
{
// 创建家的对象,相当于home = Home * home
home = new Home;
}
// 参观函数,访问Home中的属性
void visit()
{
cout << "类好朋友正在访问" << home->sittingroom << endl;
cout << "类好朋友正在访问" << home->bedroom << endl;
}
public:
Home *home;
};
int main()
{
Myfriend f;
f.visit();
return 0;
}
(3)成员函数做友元
1、所有类内函数必须类外实现。
2、Myfriend要先于Home定义,因为Home中需要将Myfriend类的visit1()函数声明为友元函数。
3、在Myfriend类前记得声明Home类,因为Myfriend类中有Home类型的指针做属性。
#include <iostream>
using namespace std;
class Home;
class Myfriend
{
public:
Myfriend();
void visit1();
void visit2();
Home *home;
};
class Home
{
// 声明好朋友类中的visit1()是友元
friend void Myfriend::visit1();
public:
Home();
public:
string sittingroom;
private:
string bedroom;
};
Home::Home()
{
sittingroom = "客厅";
bedroom = "卧室";
}
Myfriend::Myfriend()
{
// 创建家的对象,相当于home = Home * home
home = new Home;
}
// visit1()让它可以访问Home中的私有属性
void Myfriend::visit1()
{
cout << "类好朋友正在访问" << home->sittingroom << endl;
cout << "类好朋友正在访问" << home->bedroom << endl;
}
// visit2(),让它不可以访问Home中的私有属性
void Myfriend::visit2()
{
cout << "类好朋友正在访问" << home->sittingroom << endl;
// cout << "类好朋友正在访问" << home->bedroom << endl;
}
int main()
{
Myfriend f;
f.visit1();
f.visit2();
return 0;
}
4、运算符重载
运算符诞生于C语言中,如加减乘除,这些都是编译器内置的,但是到了面向对象时代(C++),就带来了新问题:两个对象如何运算?
Person p1 + Person p2显然是不行的,这就需要运算符重载了。
表面上看,运算符重载是对编译器原生运算符,我们在某个具体的类中做重定义。
本质上,是运算符被映射到一个成员函数中(+被映射到operator+()中),换句话说,编译器原生的运算符也是一个函数,而我们重新定义了这个函数。
c = a + b;
c = a.operator+(b);
基本格式:
返回值类型 operator运算符名称(形参列表)
{
对运算符的重载处理
}
(1)加减乘除运算符重载
成员函数重载:
#include <iostream>
using namespace std;
class Person
{
public:
int a,b;
Person(){}
Person(int a,int b)
{
this->a = a;
this->b = b;
}
// 重载+运算符
Person operator+(Person &p)
{
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
}
// 重载-运算符
Person operator-(Person &p)
{
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
}
// 重载*运算符
Person operator*(Person &p)
{
Person temp;
temp.a = this->a * p.a;
temp.b = this->b * p.b;
return temp;
}
// 重载/运算符
Person operator/(Person &p)
{
Person temp;
temp.a = this->a / p.a;
temp.b = this->b / p.b;
return temp;
}
};
int main()
{
Person p1(10,10);
Person p2(20,20);
Person p3 = p1 + p2;
cout << p3.a << " " << p3.b << endl;
Person p4 = p1-p2;
cout << p4.a << " " << p4.b << endl;
Person p5 = p1*p2;
cout << p5.a << " " << p5.b << endl;
Person p6 = p1/p2;
cout << p6.a << " " << p6.b << endl;
return 0;
}
全局函数重载:
#include <iostream>
using namespace std;
class Person
{
public:
int a,b;
Person(){}
Person(int a,int b)
{
this->a = a;
this->b = b;
}
// 如果成员属性是私有,就需要加上友元
friend Person operator+(Person &p1,Person &p2);
friend Person operator-(Person &p1,Person &p2);
friend Person operator*(Person &p1,Person &p2);
friend Person operator/(Person &p1,Person &p2);
};
// 重载+运算符
Person operator+(Person &p1,Person &p2)
{
Person temp;
temp.a = p1.a + p2.a;
temp.b = p1.b + p2.b;
return temp;
}
// 重载-运算符
Person operator-(Person &p1,Person &p2)
{
Person temp;
temp.a = p1.a - p2.a;
temp.b = p1.b - p2.b;
return temp;
}
// 重载*运算符
Person operator*(Person &p1,Person &p2)
{
Person temp;
temp.a = p1.a * p2.a;
temp.b = p1.b * p2.b;
return temp;
}
// 重载/运算符
Person operator/(Person &p1,Person &p2)
{
Person temp;
temp.a = p1.a / p2.a;
temp.b = p1.b / p2.b;
return temp;
}
int main()
{
Person p1(10,10);
Person p2(20,20);
Person p3 = p1 + p2;
cout << p3.a << " " << p3.b << endl;
Person p4 = p1-p2;
cout << p4.a << " " << p4.b << endl;
Person p5 = p1*p2;
cout << p5.a << " " << p5.b << endl;
Person p6 = p1/p2;
cout << p6.a << " " << p6.b << endl;
return 0;
}
注意,如果必须要在类内定义的话,只能定义为单参数的运算符函数(只能传入一个)。
所以最好是用全局函数来定义运算符重载。
(2)重载输入输出运算符
重载输入运算符>>可以自定义输入类型,重载输出运算符<<可以自定义输出类型。
如果我们想输出num,直接cout << num << endl;是不行的,因而我们需要重载<<,让编译器知道我们的num中的m_a和m_b该怎么输出,因此我们需要重载左移运算符<<。
#include<iostream>
using namespace std;
class Person
{
public:
int a;
int b;
// 重载输出输出运算符:都是友元函数实现
friend istream& operator>>(istream &ist,Person &p);
friend ostream& operator<<(ostream &ost,Person &p);
};
// 重载输入运算符
istream& operator>>(istream &ist,Person &p)
{
ist >> p.a;
ist >> p.b;
return ist;
}
// 重载输出运算符
ostream& operator<<(ostream &ost,Person &p)
{
ost << p.a << endl;
ost << p.b << endl;
return ost;
}
int main()
{
Person p;
cin >> p;
cout << p;
return 0;
}
(3)重载递增运算符
如果我们想让类中的整型变量a进行递增运算——重载递增运算符。
重载递增运算符:
#include<iostream>
using namespace std;
// 自定义整型
class Myint
{
public:
Myint()
{
m_a = 0;
}
// 重载前置++运算符,返回引用是为了一直对一个数据进行递增操作
Myint& operator++()
{
// 先做++运算
m_a++;
// 后this指针解引用,返回自身
return *this;
}
// 重载后置++运算符
// 这里的int代表占位参数,用来区分前置和后置递增函数(必须写int)
// 这里要返回的是值,不是引用,因为是引用,那就是返回的temp的引用,这是个局部变量,在做完运算后就释放掉了,这就非法了。
Myint operator++(int)
{
// 先记录当时结果
Myint temp = *this;
// 再做++运算
m_a++;
// 最后将先记录的结果做返回
return temp;
}
int m_a;
};
// 重载左移运算符
ostream & operator<<(ostream & cout,Myint myint)
{
cout << myint.m_a;
return cout;
}
// 前置递增测试
void test01()
{
Myint myint;
cout << myint++ << endl;
cout << myint << endl;
}
// 后置递增测试
void test02()
{
Myint myint;
cout << myint++ << endl;
cout << myint << endl;
}
int main()
{
test01();
test02();
return 0;
}
(4)赋值运算符重载=
C++编译器至少给一个类添加4个函数:
1、默认构造函数(无参,函数体为空);
2、默认析构函数(无参,函数体为空);
3、默认拷贝构造函数,对属性进行值拷贝;
4、赋值运算符operator=,对属性进行值拷贝。
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。
#include<iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
myage = new int(age);
}
~Person()
{
if(myage != NULL)
{
delete myage;
myage = NULL;
}
}
// 重载赋值运算符
Person& operator=(Person &p)
{
// 先判断是否有属性在堆区,如果有先释放干净,再深拷贝
if(myage != NULL)
{
delete myage;
myage = NULL;
}
// 深拷贝操作
myage = new int(*p.myage);
// 返回对象本身,便于实现a=b=c的连等操作
return *this;
}
int * myage;
};
int main()
{
Person p1(18);
Person p2(20);
Person p3(22);
// 赋值操作。下面这步,如果我们不在类中写析构函数释放myage,则不会报错且运行良好。当我们写了上面的释放代码,当p2=p1时,它们相当于指向的是同一块堆区内存,这就会导致堆区的内存重复释放。
// p2=p1;
// cout << "p1的年龄为:" << *p1.myage << endl;
// cout << "p2的年龄为:" << *p2.myage << endl;
// 解决方法是:利用深拷贝解决上面的浅拷贝带来的问题,即在堆区重新开发一块空间,其中myage仍等于18,然后p1p2各自释放。也就是说,不能用编译器提供给我们的赋值运算符=,得自己重载赋值运算符=。
// 重载之后再运行:
p3=p2=p1;
cout << "p1的年龄为:" << *p1.myage << endl;
cout << "p2的年龄为:" << *p2.myage << endl;
cout << "p3的年龄为:" << *p3.myage << endl;
return 0;
}
(5)关系运算符重载==
重载关系运算符==,让两个自定义类型对象进行对比操作。
#include<iostream>
using namespace std;
class Person
{
public:
Person(string name,int age)
{
myname = name;
myage = age;
}
// 重载==
bool operator==(Person p)
{
if(this->myname==p.myname && this->myage==p.myage)
{
return true;
}
return false;
}
string myname;
int myage;
};
int main()
{
Person p1("Tom",18);
Person p2("Jerry",18);
if(p1==p2)
{
cout << "p1和p2是相等的" << endl;
}
else
{
cout << "p1和p2是不相等的" << endl;
}
return 0;
}
同理可以重载!=、<、>等。
(6)函数调用运算符重载()
函数调用运算符()也可以重载,由于重载后使用的方式非常像函数的调用,因此称之为仿函数,它非常灵活,没有固定写法。
#include<iostream>
using namespace std;
class Myprint
{
public:
// 重载函数调用运算符
void operator()(string test)
{
cout << test << endl;
}
};
int main()
{
Myprint mp;
mp("hello world");
return 0;
}
仿函数非常灵活,没有固定写法。
#include<iostream>
using namespace std;
class Myadd
{
public:
int operator()(int a,int b)
{
return a+b;
}
};
int main()
{
Myadd myadd;
int ret = myadd(100,200);
cout << ret << endl;
return 0;
}
匿名函数对象,上面的案例我们还可以这样输出:
5、继承
(1)继承的基本语法和方式
语法:class 子类 : 继承方式 父类
这里的子类也称为派生类,父类也成为基类。
其中,继承方式有:公共继承(public)、保护继承(protected)、私有继承(private)
比如,在一个网站中,头部导航和尾部信息都是一样的,只有中间的内容不一样:
#include <iostream>
using namespace std;
class Publiccontent
{
public:
void header()
{
cout << "公共头部:首页、公开课、登录注册..." << endl;
}
void footer()
{
cout << "公共底部:帮助中心、交流合作、站内地图..." << endl;
cout << "========================================" << endl;
}
};
class Java : public Publiccontent
{
public:
void content()
{
cout << "Java学科视频..." << endl;
}
};
class Cpp : public Publiccontent
{
public:
void content()
{
cout << "C++学科视频..." << endl;
}
};
int main()
{
Java ja;
ja.header();
ja.content();
ja.footer();
Cpp cpp;
cpp.header();
cpp.content();
cpp.footer();
return 0;
}
(2)继承中的对象模型
从父类继承过来的成员属性,哪些属于子类对象中?
#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;
};
int main()
{
// 子类每创建一个对象占用多少内存空间?
cout << sizeof(Son) << endl; // 输出结果为:16,子类会全继承父类的非静态成员属性,只是有的继承了不能访问罢了。
return 0;
}
(3)继承中的构造和析构顺序
父类和子类中的构造和析构顺序是谁先谁后?
#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;
}
};
int main()
{
Son s;
return 0;
}
继承中的构造和析构顺序如下:先构造父类,再构造子类,析构则相反,先析构子类,再析构父类。
(4)继承同名成员处理方式
当子类和父类出现同名的成员,如何通过子类对象,访问到子类或者父类中同名的数据呢?
——访问子类同名成员,可以直接访问,访问父类同名成员,需要加作用域。
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
m_a = 100;
}
void func()
{
cout << "父类的成员函数调用:Base-func()" << endl;
}
int m_a;
};
class Son : public Base
{
public:
Son()
{
m_a = 200;
}
void func()
{
cout << "子类的成员函数调用:Son-func()" << endl;
}
int m_a;
};
int main()
{
Son s;
cout << "同名成员属性:" << endl;
cout << "子类的m_a属性为:" << s.m_a << endl;
cout << "父类的m_a属性为:" << s.Base::m_a << endl;
cout << "同名成员函数:" << endl;
s.func();
s.Base::func();
return 0;
}
注意,如果子类中出现和父类同名的成员函数,那么子类的同名成员函数会隐藏掉父类中搜友的同名成员函数,也就是所有父类中重载的函数,都会被隐藏,如果想访问就需要加作用域。
同名静态成员的处理:
继承中同名的静态成员在子类对象上如何访问呢?——与上述一致。
#include <iostream>
using namespace std;
class Base
{
public:
static void func()
{
cout << "父类的静态成员函数调用:Base-func()" << endl;
}
static int m_a;
};
int Base::m_a = 100;
class Son : public Base
{
public:
static void func()
{
cout << "子类的静态成员函数调用:Son-func()" << endl;
}
static int m_a;
};
int Son::m_a = 200;
int main()
{
Son s;
cout << "同名静态成员属性:" << endl;
cout << "访问方式1——通过对象访问:" << endl;
cout << "子类的静态成员属性m_a为:" << s.m_a << endl;
cout << "父类的静态成员属性m_a为:" << s.Base::m_a << endl;
cout << "访问方式2——通过类名访问:" << endl;
cout << "子类的静态成员属性m_a为:" << Son::m_a << endl;
// 注意下面,可以通过Base::m_a直接访问,但是如果是想要通过子类来访问父类中的m_a属性,就要先Son::这是代表要通过类名的方式访问,之后的Base::代表要访问Base作用域下的静态成员属性m_a。
cout << "父类的静态成员属性m_a为:" << Son::Base::m_a << endl;
cout << endl;
cout << "同名静态成员函数:" << endl;
cout << "访问方式1——通过对象访问:" << endl;
s.func();
s.Base::func();
cout << "访问方式2——通过类名访问:" << endl;
Son::func();
Son::Base::func();
return 0;
}
(5)多继承语法
class 子类 : 继承方式 父类1, 继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分。
C++实际开发中并不建议使用多继承。
#include <iostream>
using namespace std;
class Base1
{
public:
Base1()
{
m_a = 100;
}
int m_a;
};
class Base2
{
public:
Base2()
{
m_a = 200;
}
int m_a;
};
class Son : public Base1 , public Base2
{
public:
Son()
{
m_b = 300;
m_c = 400;
}
int m_b;
int m_c;
};
int main()
{
Son s;
cout << "对象s占用:" << sizeof(s) << endl;
// 当多个父类中出现同名成员,需要加作用域区分:
cout << "Base1中的m_a:" << s.Base1::m_a << endl;
cout << "Base2中的m_a:" << s.Base2::m_a << endl;
cout << "子类中的m_b:" << s.m_b << endl;
cout << "子类中的m_c:" << s.m_c << endl;
return 0;
}
(6)菱形继承
菱形继承概念:
两个派生类继承同一个基类;
又有某个类同时继承者两个派生类;
这种继承被称为菱形继承,或者钻石继承。
#include<iostream>
using namespace std;
// 动物类
class Animal
{
public:
int m_age;
};
// 羊类
class Sheep : public Animal{};
// 驼类
class Camel : public Animal{};
// 羊驼类
class Alpaca : public Sheep, public Camel{};
int main()
{
Alpaca al;
// 当出现菱形继承时,两个父类拥有相同属性,需要加作用域区分。
al.Sheep::m_age = 10;
al.Camel::m_age = 20;
cout << "从Sheep类继承下来的m_age为:" << al.Sheep::m_age << endl;
cout << "从Camel类继承下来的m_age为:" << al.Camel::m_age << endl;
return 0;
}
直接访问属性的话就会报错:
但是,上面两份数据多余了,我们只需要一份即可,因此,菱形继承会导致数据有两份,造成资源浪费。
利用虚继承,解决菱形继承的问题:关键字virtual
其中,Animal类称为虚基类。
再运行:
因为,当进行虚继承后,这份数据就只有一份了,我们相当于是先给这份数据赋值为10,再给它赋值为20,最后的结果自然也就是20。
6、多态
(1)多态的基本概念和原理
多态分为两类:
静态多态:函数重载和运算符重载属于静态多态,复用函数名。
动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态区别:
静态多态的函数地址早绑定-编译阶段确定函数地址。
动态多态的函数地址晚绑定-运行阶段确定函数地址。
多态满足条件:1、有继承关系;2、子类重写父类中的虚函数。
多态使用条件:父类指针或引用指向子类对象。
重写:函数返回值类型函数名参数列表完全一致称为重写。
下面这个例子:
#include<iostream>
using namespace std;
class Animal
{
public:
void AOAO()
{
cout << "动物嗷嗷" << endl;
}
};
class Cat : public Animal
{
public:
void AOAO()
{
cout << "猫咪嗷嗷" << endl;
}
};
// 执行函数
void doAOAO(Animal &animal)
{
animal.AOAO();
}
int main()
{
Cat cat;
doAOAO(cat); // 运行结果是:动物嗷嗷
// 这实际上是:Animal &animal = cat,即父类引用接收子类对象,这相当于是把猫这个类的对象强制转换成动物类了。
return 0;
}
最终的运行结果是动物嗷嗷,是因为地址早绑定,在编译阶段就确定了函数地址,不管传入的是什么类的对象,都会走animal.AOAO()
如果我们想执行猫咪嗷嗷,那这个函数地址就不能提前绑定,需要在运行阶段再绑定:
#include<iostream>
using namespace std;
class Animal
{
public:
// 虚函数,这个类内部就发生了改变,就可以实现地址晚绑定了。
virtual void AOAO()
{
cout << "动物嗷嗷" << endl;
}
};
class Cat : public Animal
{
public:
void AOAO()
{
cout << "猫咪嗷嗷" << endl;
}
};
// 执行函数
void doAOAO(Animal &animal)
{
animal.AOAO();
}
int main()
{
Cat cat;
doAOAO(cat);
return 0;
}
虚函数的原理:
如果我们没加virtual关键字的话:
加上virtual关键字后:(X64是8,其他是4)
![](https://i-blog.csdnimg.cn/blog_migrate/acd585d8aae17d20cd8620b644864e05.png)
实验案例:分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类。来体验一下多态的优点。
普通写法:
#include<iostream>
using namespace std;
class Calculate
{
public:
int Result(string oper)
{
if(oper == "+")
{
return a+b;
}
else if(oper == "-")
{
return a-b;
}
else if(oper == "*")
{
return a*b;
}
// 如果想要扩展新的功能,需要修改源码
// 在真实开发中,提倡开闭原则:对扩展进行开放,对修改进行关闭。
}
int a;
int b;
};
int main()
{
Calculate cal;
cal.a = 10;
cal.b = 10;
cal.Result("+");
return 0;
}
多态的实现:
#include<iostream>
using namespace std;
// 实现计算器的基类/抽象类(里面什么都不写)
class ABcalculate
{
public:
virtual int Result()
{
return 0;
}
int a;
int b;
};
// 加法计算器类
class Add : public ABcalculate
{
public:
int Result()
{
return a+b;
}
};
// 减法计算器类
class Sub : public ABcalculate
{
public:
int Result()
{
return a-b;
}
};
// 乘法计算器类
class Mul : public ABcalculate
{
public:
int Result()
{
return a*b;
}
};
int main()
{
// 实现多态:父类指针或引用指向子类对象,这里用指针。
// 实现加法:
ABcalculate * abc = new Add;
abc->a = 10;
abc->b = 10;
abc->Result();
delete abc;
// 实现减法(注意这里指针的类型没变,delete只是释放了里面的数据,没必要再创建一个)
abc = new Sub;
abc->a = 20;
abc->b = 20;
abc->Result();
delete abc;
return 0;
}
虽然代码量多了,但是仍然有很大的好处:
多态的优点:
1、代码组织结构清晰、可读性强;
2、利于前期和后期的扩展以及维护。
(2)纯虚函数和多态类
前面两个例子,一个动物和猫,一个计算器,里面父类的函数功能基本都没用到,比如“动物嗷嗷”和return 0,父类中的纯虚函数基本都没什么用,所以可以把虚函数改成纯虚函数。
纯虚函数:virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:1、无法实例化对象;2、子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func() = 0;
};
class Son : public Base
{
public:
virtual void func()
{
cout << "func()调用" << endl;
}
};
int main()
{
Base * base = new Son;
base->func();
return 0;
}
案例:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料。
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。
注:冲泡咖啡的辅料是糖和牛奶,茶是枸杞。
#include<iostream>
using namespace std;
class Base
{
public:
// 煮水:
virtual void Boil() = 0;
// 冲泡:
virtual void Brew() = 0;
// 导入杯中:
virtual void Pour() = 0;
// 加辅料:
virtual void Putsth() = 0;
// 整合以上步骤为制作饮品的函数:
void MakeDrink()
{
Boil();
Brew();
Pour();
Putsth();
}
};
// 制作咖啡类:
class Coffee : public Base
{
public:
virtual void Boil()
{
cout << "煮农夫山泉矿泉水" << endl;
}
virtual void Brew()
{
cout << "冲泡咖啡" << endl;
}
virtual void Pour()
{
cout << "倒入猫爪杯" << endl;
}
virtual void Putsth()
{
cout << "加入糖和牛奶" << endl;
}
};
// 制作茶类:
class Tea : public Base
{
public:
virtual void Boil()
{
cout << "煮百岁山矿泉水" << endl;
}
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
virtual void Pour()
{
cout << "倒入紫砂壶" << endl;
}
virtual void Putsth()
{
cout << "加入枸杞" << endl;
}
};
int main()
{
// 制作咖啡:
cout << "开始制作咖啡:" << endl;
Base * base = new Coffee;
base->MakeDrink();
delete base;
// 制作茶:
cout << "开始制作茶:" << endl;
base = new Tea;
base->MakeDrink();
delete base;
return 0;
}
(3)虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构共性:
1、可以解决上面的问题,即父类指针释放子类对象;
2、都需要有具体的函数实现。
虚析构和纯虚析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象。
正常书写会呈现的结果:
#include<iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal构造函数调用" << endl;
}
// 纯虚函数:
virtual void AOAO() = 0;
~Animal()
{
cout << "Animal析构函数调用" << endl;
}
};
class Dog : public Animal
{
public:
// 用构造函数给狗狗名字指针赋值
Dog(string name)
{
cout << "Dog构造函数调用" << endl;
dogname = new string(name);
}
virtual void AOAO()
{
cout << *dogname << "狗狗嗷嗷" << endl;
}
// 用析构函数来释放堆区数据
~Dog()
{
if(dogname != NULL)
{
cout << "Dog析构函数调用" << endl;
delete dogname;
dogname = NULL;
}
}
// 设置一个狗狗名字的指针,用来存放狗狗的名字
string * dogname;
};
int main()
{
Animal * ani = new Dog("冰冰");
ani->AOAO();
delete ani;
return 0;
}
显然没有走Dog析构函数的代码,这是因为多态使用时,父类指针在析构的时候不会调用子类中的析构函数,导致子类如果有堆区属性,会出现内存泄漏现象。
只需要把父类的析构改成虚析构即可:
纯虚析构:virtual ~Animal() = 0;
虚析构和纯虚析构只能有一个,不过如果我们真的使用纯虚析构的话,会在执行时报错,是因为纯虚析构不仅需要声明,也需要代码的实现。
也就是需要类内声明:virtual ~Animal() = 0;,类外实现(如下):
并且,假设没有设置虚函数,但是如果设置了纯虚析构, 那这个类也属于抽象类。
案例:电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)。
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口。
测试时组装两台不同的电脑进行工作。
#include<iostream>
using namespace std;
// 三个零件的抽象类:CPU、显卡、内存条
// CPU类
class CPU
{
public:
// 计算的虚函数
virtual void calculate() = 0;
};
// 显卡类
class VideoCard
{
public:
// 显示的虚函数
virtual void display() = 0;
};
// 内存条类
class Memory
{
public:
// 存储的虚函数
virtual void storage() = 0;
};
// 两个具体厂商类:Intel,Lenovo
// Intel厂商继承三个不同的零件:
class IntelCPU : public CPU
{
public:
void calculate()
{
cout << "Intel的CPU开始计算了!" << endl;
}
};
class IntelVideoCard : public VideoCard
{
public:
void display()
{
cout << "Intel的显卡开始显示了!" << endl;
}
};
class IntelMemory : public Memory
{
public:
void storage()
{
cout << "Intel的内存条开始存储了!" << endl;
}
};
// Lenovo厂商继承三个零件:
class LenovoCPU : public CPU
{
public:
void calculate()
{
cout << "Lenovo的CPU开始计算了!" << endl;
}
};
class LenovoVideoCard : public VideoCard
{
public:
void display()
{
cout << "Lenovo的显卡开始显示了!" << endl;
}
};
class LenovoMemory : public Memory
{
public:
void storage()
{
cout << "Lenovo的内存条开始存储了!" << endl;
}
};
// 电脑类:用于将以上三个(可能)来自不同厂商的零件组装起来
class Computer
{
public:
// 给类内的三个零件属性赋值:
Computer(CPU * cpu,VideoCard * vc,Memory * mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}
// 整合每个零件的工作函数:
void work()
{
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
// 提供析构函数,释放3个电脑零件:
~Computer()
{
if(m_cpu != NULL)
{
delete m_cpu;
m_cpu = NULL;
}
if(m_vc != NULL)
{
delete m_vc;
m_vc = NULL;
}
if(m_mem != NULL)
{
delete m_mem;
m_mem = NULL;
}
}
// 电脑中含有以上三个零件,以指针的形式写入:
private:
CPU * m_cpu;
VideoCard * m_vc;
Memory * m_mem;
};
int main()
{
// 组装第一台电脑:
// 第一台电脑的零件:
CPU * incpu = new IntelCPU;
VideoCard * lnvc = new LenovoVideoCard;
Memory * inmem = new IntelMemory;
// 创建第一台电脑:
Computer * computer01 = new Computer(incpu,lnvc,inmem);
cout << endl << "第一台电脑组装完成!" << endl;
computer01->work();
delete computer01;
// 组装第二台电脑:
CPU * lncpu = new LenovoCPU;
VideoCard * invc = new IntelVideoCard;
Memory * lnmem = new LenovoMemory;
Computer * computer02 = new Computer(lncpu,invc,lnmem);
cout << endl << "第二台电脑组装完成!" << endl;
computer02->work();
delete computer02;
return 0;
}
五、文件操作
对文件进行读写操作要先包含头文件<fstream>
操作文件的三大类:1、ofstream:写操作;2、ifstream:读操作;3、fstream:读写操作。
文件类型分为两种:
1、文本文件:文件以文本的ASCII码形式存储在计算机中。
2、二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们。
1、文本文件
写文件的步骤:
1、包含头文件(一般是<fstream>)
2、创建ofstream类的对象ofs(即创建流对象)
3、打开文件:ofs.open(“文件路径”,打开方式);
4、写入数据:ofs << “要写入的数据”;
5、关闭文件:ofs.close();
注意,文件的打开方式可以配合使用,利用|操作符,比如我们想用二进制的方式写文件,就可以:ios::binary | ios::out
#include<iostream>
using namespace std;
// 1、包含头文件
#include<fstream>
int main()
{
// 2、创建ofstream类的对象ofs(可以是任意名)
ofstream ofs;
///3、打开文件。这里可以写直接路径,相对路径,文件名,三个都行。如果没有这个文件会创建,像下面我们如果没有指定路径的话,会默认创建在当前项目路径下。
ofs.open("test.txt",ios::out);
// 4、写入内容:
ofs << "我的第一个c++文件操作test" << endl;
ofs << "耶耶耶!" << endl;
// 5、关闭文件:
ofs.close();
return 0;
}
读文件的步骤:
1、包含头文件(一般是<fstream>)
2、创建ifstream类的对象ifs(即创建流对象)
3、打开文件并判断是否打开成功:ifs.open(“文件路径”,打开方式);和ifs.is_open();
4、读文件数据:四种方式读取
5、关闭文件:ofs.close();
来读取上个实验中写的文件:
#include<iostream>
#include<string>
using namespace std;
// 1、包含头文件
#include<fstream>
int main()
{
// 2、创建ifstream类的对象ifs(可以是任意名)
ifstream ifs;
///3、打开文并判断是否打开成功。
ifs.open("test.txt",ios::in);
// is_open()函数会判断是否打开成功,打开成功会返回true。
if(!ifs.is_open())
{
cout << "文件打开失败!" << endl;
return 0;
}
// 4、读取文件数据:共有四种方式
char temp[1024] = {0};
// 方法一:
// ifs >> temp,当所有数据全部读到后,会返回false,退出循环。
char temp[1024] = {0};
while(ifs >> temp)
{
cout << temp << endl;
}
// 方法二:
char temp[1024] = {0};
while(ifs.getline(temp,sizeof(temp)))
{
cout << temp << endl;
}
// 方法三:注意包含string头文件
string temp;
while( getline(ifs,temp) )
{
cout << temp << endl;
}
// 方法四:EOF-end of file,判断有没有读到文件尾,读到则返回true。
char ch;
while((ch = ifs.get())!= EOF)
{
cout << ch;
}
// 5、关闭文件:
ifs.close();
return 0;
}
2、二进制文件
对于二进制文件,打开方式要指定为:ios::binary
二进制操作文件不仅可以操作内置的数据类型如int double等,还可以操作自定义数据类型。
写文件的步骤:
1、包含头文件<fstream>
2、创建ofstream类的对象ofs(可以是任意名);
3、打开文件:ofs.open(“文件路径”,打开方式);
4、写入数据:ofs.write(const char * temp,int len)
注意必须得是const char*类型的,不是的话就必须强转,如下面的例子。
5、关闭文件:ofs.close();
注:ostream & write(要写入的数据的内存空间,int len);
要写入的数据的内存空间通常这样表示:如果是数组,则是const char * temp,如果不是,则还要多一步取地址的操作,即(const char *)&p,len是读写的字节数。
二进制方式写文件主要利用流对象调用成员函数write()
#include<iostream>
#include<string>
using namespace std;
// 1、包含头文件
#include<fstream>
class Person
{
public:
char m_name[64];
int m_age;
};
int main()
{
// 2、创建ofstream类的对象ofs(可以是任意名)
ofstream ofs;
//3、打开文件
ofs.open("person.txt",ios::out | ios::binary);
// 4、写入数据
Person p = {"张三",18};
ofs.write((const char *)&p,sizeof(Person));
// 5、关闭文件
ofs.close();
return 0;
}
读文件步骤:
1、包含头文件<fstream>
2、创建ifstream类的对象ifs(可以是任意名);
3、打开文件:ifs.open(“文件路径”,打开方式);
4、写入数据:ifs.read(char * temp,int len),注意必须得是char*类型的(不需要const了),不是的话就必须强转,如下面的例子。
5、关闭文件:ifs.close();
注:istream & read(要写入的数据的内存空间,int len)
二进制方式读文件主要通过流对象调用成员函数read
读取上个实验中写的文件:
#include<iostream>
#include<string>
using namespace std;
// 1、包含头文件
#include<fstream>
class Person
{
public:
char m_name[64];
int m_age;
};
int main()
{
// 2、创建ifstream类的对象ifs(可以是任意名)
ifstream ifs;
//3、打开文件并判断是否打开成功
ifs.open("person.txt",ios::in | ios::binary);
if(!ifs.is_open())
{
cout << "文件打开失败" << endl;
return 0;
}
// 4、读取文件
Person p;
ifs.read((char *)&p,sizeof(Person));
cout << "姓名:" << p.m_name << endl;
cout << "年龄:" << p.m_age << endl;
// 5、关闭文件
ifs.close();
return 0;
}