C++核心编程

C++核心编程,第1张

1、内存的分区模型
(1)代码区:书写的所有代码(二进制形式),由 *** 作系统管理
(2)全局区:放全局变量(在函数体外的变量)、静态变量(static)和常量(const)(包括字符串常量,双引号括起来的部分取其地址:&“hello”、全局常量)
(3)栈顶:编译器管理(自动分配和释放),存参数值、局部变量(函数中的变量,包括main函数,而且局部常量也是放这)等
(4)堆区:有程序员管理,若程序员不释放,则在程序结束的时候,系统自动回收

2、内存四区的意义:不同区域存放的数据,赋予不同的生命周期,可灵活编程
3、程序编译后运行前,生成了exe文件,这时只分为两区:(所在的地址块不同,可取址看其区别)
(1)代码区
①共享(有些程序可多次执行exe),即频繁执行的程序,只在内存中一份代码即可
②只读:防止程序意外修改他的指令
(2)全局区:该区域的数据,在程序结束后由 *** 作系统释放

4、程序运行后:
(1)栈区:由编译器自动分配释放,存放函数的参数值,局部变量
①注意:不要返回局部变量的地址(一般是返回值),栈区开辟的数据由编译器自动释放
1)因为局部变量存放在栈区,栈区的数据在函数执行完之后自动释放(会乱码)
2)不过第一次可以打印正确地数字,是因编译器做了次保留,第二次就错了

(2)堆区:由程序员分配释放,若程序员不释放,在程序结束是有 *** 作系统回收
①C++中用new在堆区开辟内存(int* p = new int(10);)
1)//new 返回的是该类型的指针
2)int* arr = new int[10];//注:这里是方括号,里面的是表示数组的大小,若不是方括号,则该框中的值为要保存的值
②用堆区,可以返回局部变量的地址,这样,这部分地址在退出函数时,则还没有释放
③释放方法:delete
1)delete p;//释放普通类型的数据
2)delete[] arr;//释放数组的时候,要加[]才行

5、引用:
(1)本质:给一变量起别名,一起 *** 控同一块内容
(2)注意事项:
①引用必须初始化
②初始化后,则无法再修改(即只能做一个变量的别名)

(3)引用做函数参数:(引用传递:C++推荐使用这种方式)
①可通过形参修改实参,从而替代指针,简化指针需要的 *** 作(用指针做形参时,调用函数所用变量的前面,还需加取地址符号&)
②总结:函数的参数传递
1)值传递
2)指针传递
③引用传递void test1(int &a,int &b){}(在函数的定义时设置引用就可以了)
1)效果和指针传递的效果一样,但比指针传递简单

(4)引用做函数的返回值 int& test02(){}
①不要做局部变量的返回值(因为这部分内存在该函数执行完之后,会释放掉,返回的别名也就没啥意义了)
②如果函数的返回值为引用,那么,函数的调用可以作为左值(即等号的左边)
1)test02() = 1000;,因为返回的是引用类型,是一变量名,这样的话,就是在给该变量做赋值 *** 作,修改了该引用指向的内存空间的值

(5)引用的本质是一个指针常量,因而指向的对象不可改,但里面的值是可以改的
int a = 10;
int& ref = a;//这里其实会自动转化为int* const ref=&a;
① ref = 20;//这里编译器内部自动转化为*ref=20;

(6)常量引用:主要用来修饰形参,防止误 *** 作 void showData(const int &a) {}
①使得在该函数体中无法修改该值,同时,在其他函数中(如main函数)中确实可以改的
//int& ref = 10;//直接将引用指向自变量的话,会报错,
//因为引用必须指向一块合法的内存空间
const int& ref = 10;//在引用前面加上const就可以
//这里加上之后,编译器会进行相应的修改:int temp=10; const int & ref=temp;
②//但这时,引用就只能是只读的状态了,不可修改

6、函数的提高
(1)函数的默认参数(有默认值的形参,要放到最右边)
①调用函数的时候,若没有传对应参数的值,则用默认参数
②如果函数的声明有了默认参数,则函数实现(定义)就不能有默认参数了(否则会重定义的了默认参数,运行时出错)
③函数声明和定义部分,只能有一个位置有默认值

(2)函数的占位参数:
①形参只写了个类型在那里,没写变量名,但调用的时候,必须传相应类型的参数
②目前阶段的占位参数,还用不到,后面再讲
③占位参数,可以是默认参数

(3)函数重载
①需满足的条件:
1)函数都在同一个作用域下
2)函数名称相同
3)函数参数类型不同、或个数不同、或顺序不同
②注意事项:函数的返回值不可以作为函数重载的条件
③引用作为重载的条件时:
1)void fun(int& a) fun(b);以变量a 进行调用的时候,调用的是第一个,相当于给引用赋值:int & a=b;
2)void fun(const int& a) fun(10),直接以常量进行调用的话,调用的是第二个,效果为,int temp=10;const int &a=temp;
④函数重载遇到默认参数
1)void test(int a, int b = 10)
2)void test(int a)
3)test(a);只有一个参数调用的时候,则会报错,因为出现二义性,两者都可以调用,test(a, b);故只能用两参数,调用含默认参数的部分

7、类和对象(封装、继承、多态)
(1)三种访问权限
①Public: 类内可以访问,类外(即用具体对象)也可以访问
②Protected: 类内可以访问,类外不可以访问(继承时,子类可以访问父类该类型的属性)
③Private 类内可以访问,类外不可以访问(子类不可访问)

(2)Struct 和class的区别(两者没有明显的区别,主要是默认权限)
①Struct默认权限是public
②Class默认权限是private

(3)一般成员属性都设置为私有
①优点:
1)可以控制成员属性的读写权限(即设置验证或判断关卡)
a.若只读,则只提供读的接口函数,不提供写的接口即可
2)对于写程序,在写入之前,还可以检测数据的有效性(即判断数据是否合理)
②注:void setw(int w){} 给属性设置值的时候,若形参的名字和属性的名字一样,则用this->w=w; 若名字不同,则可以直接赋值就可以了

(4)两个同一类的对象,赋值的时,直接等就可以了
(5)(#pragma once防止头文件重复包含)
(6)函数的声明和实现分开的时候,函数实现时int Point::getX(){ return x;}中的Point::是类名,作用是对该函数加入作用域

(7)构造函数(初始化,创建对象时自动调用)和析构函数(清理,保证安全)
①程序员不写,则系统有空实现(也即没设置内容),不过写了也是系统自动调用的,只会调用一次
②构造函数可通过参数进行重载(析构函数则不能有参数,也即不能重载,销毁前自动调用)
③按有无参数分类
1)无参构造(默认构造)
2)有参构造(给属性值初始化)
④按类型分类:
1)普通构造函数
2)拷贝构造函数Person(const Person& p) ()//拷贝构造函数,防止修改,故用const,一般用引用传参,节省空间
⑤调用有参的构造函数
1)括号法 Person p1(10);
a.注意调用无参构造函数创建对象的时候,不要在后面加括号(如Person p1();),因会被编译器视为函数的声明而不会创建类的对象
2)显式法 Person p1=Person(10);
a.单独Person(10);则是匿名对象,其特点为:当前行结束后,马上析构,因为没有名字,后面无法利用,故直接销毁
b.注意:不要利用拷贝构造初始化匿名对象:Person (p1); 因为它会被认为Person (p1)相当于Person p1; 而p1在前面已经创建了,会报错重定义
3)隐式转换法 Person p1= 10; 编译器会默认转化为:Person p1= Person(10)

(8)拷贝构造函数调用的时机:
①使用已经创建好的对象初始化一个新的对象(区别于创建的时候的等号)
②值传递的方式传参
③以值的方式返回局部对象

(9)构造函数的调用规则
①创建一个类的时候,系统会至少给类添加3个类
1)默认构造函数
2)默认析构函数
3)默认拷贝构造函数(直接对属性值复制)
②若自定义了有参构造函数,则编译器不会提供默认构造函数(无参),但提供拷贝构造
③若自定义了拷贝构造,则不提供其他构造函数(包括默认构造函数)
1)也即若自定义了拷贝构造函数,而不写其他普通的构造函数,则会导致无法构建对象

(10)深拷贝和浅拷贝
①浅拷贝(编译器提供的,直接赋值):
1)问题:堆区内存重复释放(指针部分),浅拷贝问题由深拷贝解决(重新开辟一块空间)
2)问题概述:也即有部分属性要存放到堆区时(先进后出)(int *m_height;//把身高整到堆区)(构造函数中:m_height = new int(height);),若直接用编译器提供的浅拷贝,则在Person p2(p1);利用p1赋值的时候,直接是m_height=p1.m_height;由于m_height值是地址,故两者最终指向的是同一块地址;由于new创建的是堆区数据,所以,程序员需要在析构函数中手动释放
if (m_height!=NULL)
{delete m_height;//程序员要手动释放堆区的代码
m_height = NULL;},
故需调用delete m_height;但由于是两个对象p1 p2,故会调用两次,但两 者指向的是同一个空间,释放两次,则会报错
②深拷贝(堆区的数据重新申请内存)
Person::Person(const Person& p) {
m_Age = p.m_Age;
m_height = new int(*p.m_height);} 这里p.m_height是指针,故需解指针, 用该值在堆区重新开辟一块空间

(11)初始化列表:用于初始化类的属性
①Person(int a,int b,int c):m_a(a),m_b(b),m_c©{…},替代构造函数,注意冒号的位置
(12)类对象A作为另一个类B的成员(得先定义A,才能定义另一个类B)
①构造时,先构造A再构造B
②析构的时候,则相反

(13)类中成员变量和成员函数分开存储(而且非静态变量才属于对象上面的)

class Person
{
	int m_A;//非静态成员变量,属于类对象上的,sizeof(p)=4
	static int m_B;//静态成员变量,不属于类的对象上的(也即计算对象空间的时候,不包含这块空间大小),只有一份
	void func(){}//非静态成员函数,和属性是分开存储的,不属于对象上的,只有一份,也即多个对象调用同一份函数
	static void fun2() {};//不属于对象上的
}
class Person{};
void test()
{
	Person p;
	//空对象占用的内存空间为:1,也即类中的成员为空是
	//c++编译器会给每个空对象分1字节空间,是为了区分空对象占内存的位置
	//每个空对象也应该有一个独一无二的内存地址(即不同空对象也要进行区别)
cout << "size of=" << sizeof(p) << endl;
}

(14)this指针:对象指针,隐含在每个非静态成员函数内,不需定义,直接使用
①用途
1)函数的形参和属性同名时
2)在类的非静态成员函数中,返回对象本身,可使用return *this;

Person2& personAdd( Person2& p) {
		//注意这里返回本体,要用引用的方式返回,否则就只是返回一个匿名对象
		this->age += p.age;
		return *this;//this 是指针,这里要返回对象,故需要*号解指针
	}
//可进行如下调用:
Person2 p1(10);
	Person2 p2(20);
	p2.personAdd(p1).personAdd(p1).personAdd(p1);
//链式编程思想(平时用cout也是该思想)

(15)Const 修饰成员函数(常函数):常:也即只读
①常函数内不可以修改成员属性
②如真的要修改,则需在成员属性上加关键字mutable

//this 指针的本质是指针常量(Person * const this,指针的指向是不可以修改的,
	//再在前面加上const,则该指针指向的值也不可以修改了
	//在成员函数后面加const,修饰的是this指针,让指针指向的值也不能修改
	void showPerson()const {
		//m_A = 100;//不可修改
		this->m_b = 100;//定义的时候加了mutable关键字,所以可以修改
	}
	int m_A;
	mutable int m_b;

(16)声明对象前加const,称为常对象
①常对象只能调用常函数

const Person4 p;//加const,变为常对象,不可修改其属性,
	//但可修改mutable修饰的属性
	//常对象只能访问常函数(因为常对象不可修改成员属性,
	//而普通成员函数是可以修改成员属性,相互矛盾,故不可调用)

(17)友元(friend)
①目的:让一个函数或者类访问另一个类的私有成员
②友元的三种实现
1)全局函数做友元

//在类中公有属性部分说明这个是我的好朋友,可以访问私有成员
friend void goodGay(Building& building);//全局函数

2)类做友元

//在被调用的类中,说明一下这个调用本类的类是友元类,在该类中的本类属性,可以访问私有成员
friend class GoodGay;

3)成员函数做友元

//成员函数做友元,注意:要注明是哪个类的成员函数
friend void GoodGay::test();

(18)注意:在类的使用过程中,若B类中使用到类A,需在B类前面先声明类A,在类B后面再实现类A(当然,类A在类B前面实现也可)
①要注意:类B成员函数要类外实现,并放在类A的实现的后面,这样才不会报错

class Building;//先声明类Building
class GoodGay
{
public:
	GoodGay();
	~GoodGay();
	Building* building;
};
class Building
{
public:
	Building();
	~Building();
	string m_SittingRoom;
	string m_BedRoom;
};
//后面再进行GoodGay和Building类的成员函数的类外实现

8、运算符重载
(1)加号运算符重载(将函数名改为编译器的加号名operator+即可)
①通过成员函数重载+号

//1、成员函数运算符重载
Person Person::operator+(Person& p)
{
	Person temp;
	temp.m_A = m_A + p.m_A;
	temp.m_B = m_B + p.m_B;
	return temp;
}

②通过全局函数来重载+号

//2|全局函数运算符重载
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 = p1.operator+(p2);//成员函数运算符重载:该运算符号重载调用的实质
//Person p3 = operator+(p1, p2);//全局函数运算符重载:调用的实质
Person p3 = p1 + p2;//利用编译器的函数命名operator+,可使调用简化为 + 
cout << p3.m_A << "  " << p3.m_B << endl;

④注意:
1)不可重载已有数据类型的符号运算方法,也即对内置的数据类型的表达式的运算符是不可改变的
2)当然,也不要滥用运算符重载,即别把人家加号,在内部做成了减号
(2)左移运算符重载<<
①成员函数重载operator
②全局函数重载

//1、利用成员函数进行左移运算符重载
void operator<<(ostream& cout)//由于全局只能有一个cout,所以这里需要用引用&
{    //调用的效果是:p1.operator<<(cout),简化版则是p1<
	}
//2、利用全局函数进行左移运算符的重载,返回cout则可以利用链式法则继续输出
ostream& operator<<(ostream& cout,Person &p1)
{
	cout << "m_A=" << p1.m_A << "  m_B=" << p1.m_B;
	return cout;
}

③注:如果属性是私有属性,则可以将该重载函数设置为类的友元函数
(3)自增运算符重载++(也即对自定义的类起作用)
①前置++

//重载前置++运算符,注意,这里返回的是引用,
	//因为想返回同一对象,然后可以对他进行多次++运算
	MyInteger& operator++() {
		m_Num++;
		return *this;
	}

②后置++

//重载后置++运算符,注意返回的不是引用,因为temp是局部变量,返回引用这是非法 *** 作
	MyInteger operator++(int) {//这里的int 做占位符,以做前、后置++的重载区分
		//先记录当前的值
		MyInteger temp = *this;
		//然后递增
		m_Num++;
		//接着返回开始记录的值,这样内部已经是加1后的,而返回的值不是加1后的,符合后置++的定义,这里返回的是值,而不是指针,所以函数设置的返回值类型不能是引用
		return temp;
	}

③注:对于后置++或后置–,其输出的左移运算符重载,传入的对象不能是引用,若是引用,需在前面加const(因为myint++的返回值是一个const限定的右值)

 ostream& operator<<(ostream& cout,const MyInteger &myint)
 {//也即这里的myint对象若用引用的话,需在前面加const,防止该对象在输出中被修改,或直接不用引用也可
	 cout << myint.m_Num;
	 return cout;
 }
 void test() {
	 MyInteger myint(10);
	 cout <<myint-- << endl;//直接对某对象的++或--的重载过程中
 }

(4)注:c++编译器至少给一个类添加4个函数
①默认构造函数
②默认析构函数
③默认拷贝函数person p2(p1);即创建类对象的时候使用
④赋值运算符operator=,对属性进行值拷贝(只是浅拷贝,若有属性指向堆区,则有深浅拷贝的问题)

(5)赋值运算符重载=

 //赋值运算符重载
	 Person& operator=(Person& p)
	 {
		 //先判断本身是否创建堆区,若是,需先清理干净
		 if (m_Age!=NULL)
		 {
			 delete m_Age;
		 }
		 //接着获取传进来对象的值,重新创建一堆区
		 m_Age = new int(*p.m_Age);
//编译器提供的是浅拷贝,也即m_Age=p.m_Age;因为是在堆区,所以在释放空间的时候就会出现重复释放的问题
		 return *this;//传回本身,目的在于可以重复赋值(也即连等 *** 作),a=b=c;否则就只能一部分a=b;
	 }
 }

(6)关系运算符重载
①==

 //关系运算符重载==
	bool operator==(Person& p) {
		if (m_Name==p.m_Name&&m_Age==p.m_Age)
		{
			return true;
		}
		return false;
	}
	

②!=

//关系运算符重载!=
	bool operator!=(Person& p) {
		if (m_Name == p.m_Name && m_Age == p.m_Age)
		{
			return false;
		}
		return true;
	}	

(7)函数调用重载()
①函数重载的调用,像普通函数的调用,所以又叫作仿函数,其应用非常灵活,形式不固定,具体实现的功能由其参数和函数体的内容决定
②用于打印字符串

class Myprint
{
public:
	//函数调用运算符重载
	void operator()(string test) {
		cout << test << endl;
	}
};
void test() {
	Myprint myprint;
	myprint("hello,word");//myprint.operator()("hello,word");本质
	//该重载函数调用的时候,由于像普通函数调用,所以又叫仿函数	
}

③用于打印字符串

class AddData
{
public:
	int operator()(int a, int b) {
		return a + b;
	};
};
void test1() {
	AddData add;
	cout << add(1, 2) << endl;
	cout << AddData()(2, 4) << endl;//这里是用默认函数调用
}

9、继承:
(1)继承的书写格式Class python :public BasePage{};//这里的public是继承方式
(2)继承的好处:减少重复的代码
(3)分类
①公有继承:父类中是相关属性是什么权限,在子类中也是什么权限,只是子类中不可访问父类中私有属性(相当于父类中的隐私的东西)
②保护继承:父类中的public属性,继承下来都是protected属性,父类中的私有属性一样不可访问
③私有继承:父类中的public、protected属性都变为private属性,同样父类的private属性不可访问

(4)注意:子类在继承的时候,除了静态成员属性外,其他的属性都继承,其中父类中的private属性只是被编译器隐藏了,但是有继承了

//如何打印输出某子类的中的所有成员属性,及其对应的父类
//1、打开vs的开发人员命令提示工具(Developer Command Prompt for VS)
//2、进入到该类所在的文件,所在的文件夹下(用cd进入,用dir显示当前文件夹所有文件)
// 3、输入命令:cl /d1 reportSingleClassLayout+类名 文件名+后缀名
// 注意:d后面是数字1,然后后面的一系列意思是“报告单个类的布局”

(5)继承中,创建子类对象时,先构造父类再构造子类,析构子类对象时,是先析构子类,再析构父类
①也即析构顺序和构造顺序是相反的
(6)同名成员处理(即父类和子类中有同名的属性或同名的函数)
①子类对象可以直接访问到子类中同名成员
②子类对象加作用域(即父类名::)访问到父类中的同名成员
③当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数(包括同名的重载函数),也是加作用域方可访问

void test()
{
	son s;
	cout << "调用子类中的同名成员"<<s.m_A << endl;
	cout <<"调用父类中的同名成员"<< s.Base::m_A << endl;
	//其中Base是父类名
}

(7)静态同名成员处理(即父类和子类中有同名的属性或同名的函数,且是静态的)
①处理方式和上述非静态成员一样,不过它有两种
1)通过对象访问
2)通过类名访问

void test()
{
//通过对象访问
	son s;
	cout << "调用子类中的同名成员"<<s.m_A << endl;
	cout <<"调用父类中的同名成员"<< s.Base::m_A << endl;
	//其中Base是父类名

//通过类名访问
	cout << "调用子类中的同名成员"<<son::m_A << endl;
	cout <<"调用父类中的同名成员"<< son::Base::m_A << endl;
//也可直接通过父类访问访问,只是含义不同,这样就不属于继承部分
cout <<"调用父类中的同名成员"<<Base::m_A << endl;
}

(8)多继承语法
①c++支持多继承,但在实际开发中不建议使用多继承,因为多个父类中可能会有同名成员,为区别,在调用的时候需要加作用域

void test()
{
son s;
	cout << "继承base1 的m_A:" << s.Base1::m_A << endl;
	cout << "继承base2的m_A:" << s.Base2::m_A<< endl;
}

(9)菱形继承(两个父类拥有相同的数据,需要加作用域区分)
①该继承导致两个父类的相同的数据有两份,导致资源浪费
②利用虚继承来解决该问题
1)(在父类(菱形的中间层)继承的前面加virtual 即virtual public Animal),这样,其子类多继承的时候,两个父类中相同的数据就留一份,这样可以使用子类对象直接访问,而不用父类作用域了,因为没有了二义性
2)父类的父类(也即爷爷)叫虚基类

class Animal  //虚基类
{
public:
	Animal() {
		m_A = 100;
	};
	int m_A;
};
class sheep:virtual public Animal{};   //虚继承

class tuo :virtual public Animal{};   //虚继承

class sheeptuo:public sheep,public tuo
{};
void test()
{
	sheeptuo s;
	cout << s.sheep::m_A << endl;
	cout << s.tuo::m_A << endl;
	cout << s.m_A << endl ;  //使用虚继承后,可以直接用子类访问该数据(即其两父类共同从爷爷那继承下来的数据)
}

3)使用虚继承后,子类通过父类继承的爷爷类的元素中,其实只是继承了个虚基类指针(vbptr),其指向虚基类列表(vbtable),在虚基类列表中,只存一份该数据

10、多态 (c++开发提倡利用多态设计程序架构,因为多态的优点多)
(1)多态的基本语法(动态多态)
①使用多态满足的条件
1)有继承关系
2)子类重写父类的虚函数(注意不是重载,重写是函数参数都一样)
②多态的使用
1)父类的指针或引用 执行子类的对象
2)即参数中写父类的指针或引用,传实参的时候,传的是子类的对象

class Animal
{
public:
	virtual void speak() { 
		cout << "animal 的 speak()" << endl;
	}
};
class sheep:public Animal
{
public:
	void speak() {
		cout << "sheep 的 speak()" << endl;
	}
};
//animal 中的speak函数前面若不加virtual,则会导致地址早绑定,在编译阶段就确定
//加了之后,则可以使地址晚绑定,即在运行阶段才能确定这个speak()是那个类的speak()
void dospeak(Animal& animal) {
	animal.speak();
}
void test() {
	sheep s;
	dospeak(s);
}

3)原理分析
a.对于animal类而言,若不加virtual,其整个类的大小为1字节,若加上之后,则为4字节,为虚函数指针(vfptr),指向虚函数表(vftable),虚函数表中记录的是&animal::speak
b.对于sheep类而言,由于继承并重写了speak函数,所以对应的虚函数指针指向的虚函数表中,记录的内容是&sheep::speak
c.在调用的时候,父类指针或引用指向的子类对象的时候,发生多态(这时候调用的是子类的speak函数)

(2)多态的优点
①代码组织结构清晰
②可读性强(和结构清晰有关)
③利于前期和后期的扩展和维护(即添加新功能的时候,可遵循开闭原则)

//基类
class AbstractCaculate
{
public:
	virtual int getresult() 
	{
		return 0;
	}
	int m_A;
	int m_B;
};

//加法
class add:public AbstractCaculate//每添加一个功能直接添加一个类即可
{
public:
	virtual int getresult()//这里子类中的virtual可写可不写
	{
		return m_A + m_B;
	}
};

//减法
class sub :public AbstractCaculate
{
public:
	virtual int getresult()//这里子类中的virtual可写可不写
	{
		return m_A - m_B;
	}
};

void test1() {
	AbstractCaculate* abc = new add;
	//用父类的指针或引用指向子类对象(这里用了堆区,故使用完之后,要用delete释放
	abc->m_A = 60;
	abc->m_B = 50;
	cout <<"加法:"<< abc->getresult() << endl;
	delete abc;

	abc = new sub;
	abc->m_A = 60;
	abc->m_B = 50;
	cout << "减法" << abc->getresult() << endl;
	delete abc;
}

(3)纯虚函数和抽象类
①多态中,父类的虚函数的实现是没意义的,一般都是调用子类重写的内容
②故可将该虚函数写为纯虚函数 virtual void spead() = 0; 而含有纯虚函数的类则叫抽象类
1)作用:使得子类必须重写该纯虚函数,否则,无法实例化对象
③抽象类的特点
1)无法实例化对象
2)子类必须重写抽象类中的虚函数,否则也属于抽象类,即无法实例化子类的对象,创建子类对象的时候会报错

(4)虚析构和纯虚析构
①应用场景:多态+子类有属性开辟到堆区
②解决的问题:子类中有属性开辟到堆区的时候,父类指针在释放时无法调用到子类的析构代码,会导致子类内存泄漏(也即多态使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码)
③解决方式:将父类中的析构函数改为虚析构或纯虚析构
④虚析构:直接在析构函数前面加virtual即可 virtual~Animals();
⑤纯虚析构:在虚构函数前面加virtual,然后后面=0,即virtual~Animals()=0;
1)但这里只是声明他是纯虚析构函数,也即声明他是抽象类,不能创建其对应的对象
2)由于析构的时候,也要调用父类的析构函数(即释放父类本身),所以,还要在类外实现该纯虚析构函数,否则会报错

//如这里用delete清理的父类在堆区占用的内存,若父类的析构函数不适用虚函数,则无法调用子类的析构函数子类在堆区的数据清理掉
//即在子类中含有在堆区的数据,并且调用子类时,使用父类指针指向子类对象时(多态时),才需要将父类的析构函数设置为虚析构或纯虚析构
void test() {
	Animals* ani = new Cat;
	ani->speak();
	delete ani;
}

11、文件 *** 作
(1)应用场景:程序运行时产生的数据都是临时数据,一旦运行结束都会被释放,所以为了保存这些数据,需要保存到文件中
(2)文件的类型
①文本文件:即以ASCLL码的形式保存,用户直接看就知道写了啥
②二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它(虽然并没有进行加密)
(3) *** 作文件的三大类
①Ofstream 写 *** 作
②Ifstream 读 *** 作
③fstream 读写 *** 作

(4)写文件步骤

#include
void test()
{
	//写文件的步骤:
	// 1、引入头文件
	// 2、穿件文件流对象
	ofstream ofs;
	// 3、打开文件(注:若打开模式有多项,则用 | 隔开)
	ofs.open("test.txt", ios::out);//以写的模式(ios::out)写入test.txt,没写绝对路径,则在本文件所在路径
	// 4、写入内容
	ofs << "前路漫漫雨纷纷" << endl;
	ofs << "谁在痴痴等" << endl;
	// 5、关闭文件
	ofs.close();
	//
}

(5)文件打开方式:
ios::in 为读文件而打开
ios::out 为写文件而打开
ios::ate 初始位置:文件尾
ios::app 追加方式写文件(也即在文件尾)
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

(6)读文件 *** 作

void test() {
	//读文件的 *** 作
	// 1、引入头文件
	// 2、创建读 *** 作数据流
	ifstream ifs;
	// 3、打开要读的文件,并判断是否打开成功
	ifs.open("test.txt", ios::in);
	if (!ifs.is_open()) {
		cout << "打开文件失败" << endl;
		return;
	}
	// 4、读入文件(四种方式)
第一种
	//char buf[1024] = { 0 };
	//
	//while (ifs>>buf)//把文件内容全部读入buf中,读完后,返回真,再输出
	//{
	//	cout << buf << endl;
	//}
// 第二种方式
	//char buf[1024] = { 0 };
	//while (ifs.getline(buf,sizeof(buf)))//一行行获取,第二个参数为大小
	//{
	//	cout << buf << endl;
	//}
第三种方式
	//string buf;
	//while (getline(ifs, buf))//直接用全局函数getline,输入到字符串buf中
	//{
	//	cout << buf << endl;
	//}
//第四种方式
	//即一个字符一个字符输出,一般不用这种方式,因为慢
	char c;
	while ((c=ifs.get())!=EOF)//EOF 表示end of file,即文件末尾
	{
		cout << c;
	}
	// 5、关闭文件
	ifs.close();
}

(7)二进制写文件 *** 作

#include
class Person
{
public:
	Person(string name,int age) {
		m_Name = name;
		m_age = age;
	}
	string m_Name;
	int m_age;
};
void test()
{
	//1、引入头文件
	// 2、创建流对象
	ofstream ofs;
	// 3、打开文件
	Person per("小小", 12);
	ofs.open("person.txt", ios::out | ios::binary);
	// 4、以二进制的模式写入内容
	ofs.write((const char*)&per, sizeof(per));
//注意,要报读入的数据强制转化为char型,并以指针的形式,第二个参数是大小
	// 5、关闭文件
	ofs.close()
}

(8)二进制读文件

#include
//二进制写文件的时候,除了可以写默认的数据类型外,也可以写自定义的数据类型
class Person
{
public:
	char m_Name[64];//写文件的时候,一般不直接用string,防止读数据的时候出错
	int m_age;
};
void test()
{
	//1、引入头文件
	// 2、创建流对象
	ifstream ifs;
	// 3、打开文件,并判断是否打开成功
	ifs.open("person.txt", ios::in | ios::binary);
	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		return;
	}
	// 4、以二进制的模式读入内容
	Person per;
	ifs.read((char*)&per, sizeof(per));
	//ofs.write((const char*)&per, sizeof(per));//注意,要报读入的数据强制转化为char型,并以指针的形式,第二个参数是大小
	cout << "姓名:" << per.m_Name << ",年龄:" << per.m_age << endl;
	// 5、关闭文件
	ifs.close();
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/langs/675981.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存