-
内存分区
1. 代码区:存放函数体的二进制代码 1. 全局区:存放全局变量和静态变量以及常量 2. 栈区:编译器自动分配释放,存放函数的参数值,局部变量 3. 堆区:由程序员分配和释放;
-
程序运行前,分为两个区域
1. 代码区:共享,只读 2. 全局区:全局变量和静态变量以及常量(字符串常量和其他常量),该区域的数据在程序结束后由操作系统释放
-
程序运行后
1. 栈区:编译器自动分配释放,存放函数的参数值,局部变量 注意:不要返回局部变量的地址,因为局部变量在函数调用后释放了,属于野指针。 2. 堆区:由程序员分配和释放; C++中主要利用new在堆区开辟内存 delete释放内存 i. new操作符: 1. 语法:new 数据类型 2. 会返回数据对应类型的指针 3. new int[10];//代表在堆区创建10个整型空间 4. new int(10); //代表在堆区创建一个整型空间,初始值为10
4. 引用
1. 作用:给一个变量起别名;
2. 语法:数据类型 &别名 = 原名;
3. 注意事项:引用必须初始化、引用一旦初始化,就不可以更改;
4. 引用作为函数参数:会改变实参的值,和地址传递一样,操作了同一块内存。
5. 引用作为函数返回值:
注意:不要返回局部变量的引用
函数的调用可以作为左值
6. 引用的本质:引用的本质在c++内部实现是一个指针常量
int &b = a; //相当于 int \* const b = \&a;
7. 常量引用:const int &ref = 10;
5. 函数提高
1. 函数默认参数:编写函数得时候参数可以给默认值。调用的时候如果我们自己传入数据,就用自己的数据,如果没有,就用默认值。调用的时候可以省略有默认值得参数,函数执行会使用参数得默认值
2. 注意:如果某个位置有了默认参数,那么从这个位置往后,都必须有默认值。
函数的声明和实现只能一个有默认参数,否则会出现二义性。
3. 函数的占位参数:C++函数的形参列表可以有占位参数,用来占位,调用时必须填补该位置
eg:int add(int a, int b, int){return a+b};
4. 函数重载:函数名可以相同,提高复用性
1. 满足条件:同一个作用域下,函数名称相同,函数参数类型不同或者个数不同或者顺序不同,返回值类型不可以作为重载函数的条件
2. 注意事项:
1. 引用作为重载函数参数--int &a 和const int &a时不同的类型
2. 函数重载碰到默认参数
3. 本质就是函数调用的时候会根据参数选择唯一的一个合法函数作为执行函数,其余函数的调用都是非法的。
-
类和对象
-
C++面向对象的三大特性:封装、继承和多态
-
多态的不同表现形式:重载和覆盖
-
封装的意义:将属性和行为作为一个整体,并将属性和行为加以控制
-
语法:class 类名{访问权限 属性/行为}
-
访问权限:
-
public 类内可以访问,类外可以访问
-
private 类内可以访问,类外不可以访问
儿子不可以访问父亲的私有内容 -
protected 类内可以访问,类外不可以访问
儿子可以访问父亲的保护内容
-
-
struct和class的区别:
-
struct默认权限为公共
-
class默认权限为私有
-
-
成员属性私有化:
-
优点
-
可以自己控制读写权限(将属性的读写写成函数)
-
对于写权限,可以检测数据的有效性
-
-
-
对象的初始化和清理
-
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用;
-
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
-
如果我么不提供构造和析构,编译器会提供构造函数和析构函数的空实现。
-
构造函数语法: 类名(){}
-
构造函数,没有返回值也不写void
-
函数名称与类名相同
-
构造函数可以有参数,因此可以发生重载
-
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
-
-
析构函数语法: ~类名(){}
-
析构函数,没有返回值也不写void
-
函数名称与类名相同,在名称前加上符号~
-
析构函数不可以有参数,因此不可以发生重载
-
程序在对象销毁前会自动调用析构,无须手动调用.而且只会调用一次
-
-
构造函数的分类及调用
-
分类:
-
按参数分类:有参构造和无参构造
-
按类型分类:普通构造和拷贝构造
-
-
调用
-
括号法—调用默认构造函数不要加括号,只加括号会被编译器理解为函数声明;其他的有参构造可以使用括号加上参数。
-
显示法
eg: Person p1 = Person(10);
//Person(10)是一个匿名对象,当前行执行后,系统立即回收掉匿名对象不要利用拷贝构造函数 初始化匿名对象
-
-
隐式转换法
Person p1 = 10; // 相当于Person p1 = Person(10);
-
-
拷贝函数的调用时机:
-
使用一个已经创建完毕的对象来初始化一个新对象
-
值传递的方式给函数参数传值
-
以值方式返回局部对象
-
-
构造函数的调用规则:
-
C++编译器至少给一个类添加4个函数
-
默认构造函数(无参,函数体为空)
-
默认析构函数(无参,函数体为空)
-
默认拷贝函数,对属性进行值拷贝
-
赋值运算符operator=,对属性进行值拷贝(后面会讲到)
-
-
调用规则:
-
如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝函数;
-
如果用户定义拷贝构造函数,C++不会提供其他构造函数。
-
-
-
浅拷贝和深拷贝
1. 浅拷贝:简单的赋值拷贝操作2. 深拷贝:在堆区重新申请空间,进行拷贝操作
3. 注意:如过类成员函数在堆区进行开辟内存,并且进行了浅拷贝,在释放过程中会出现多次释放而报错,解决方法是深拷贝,即重新编写拷贝函数,对开辟内存的指针每次拷贝的时候不是赋值操作,而是开辟内存操作。
-
初始化列表
- 语法:构造函数():属性1(值1),属性2(值2),属性3(值3),属性4(值4)…{}
-
类对象作为类成员
-
eg:B类中有对象A作为成员,A为对象成员
-
问题:当创建B对象时,A与B的构造和析构函数的顺序是什么?
-
当其他类对象作为本类成员,构造时候先构造类对象成员,再构造自身。
-
析构顺序与构造顺序相反
-
-
静态成员–既可以通过对象访问,也可以通过类名访问,因为静态成员不属于某一个特定对象,时所有对象共享的
-
静态成员变量
-
所有对象共享同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化
-
-
静态成员函数
-
所有对象共享同一个函数
-
静态成员函数只能访问静态成员变量
-
-
-
-
C++对象模型和this指针
-
成员变量和成员函数分开存储
-
只有非静态成员变量属于类的对象上
- C++编译器会给每个空对象分配一个字节的空间,是为了区分空对象占内存的位置
-
每个空对象也应该有一个独一无二的内存地址
-
-
this指针
-
this指针指向被调用的成员函数所属的对象
-
this指针是隐含每一个非静态成员函数内的一种指针
-
this指针不需要定义,直接使用即可
-
用途:当形参和成员变量同名时,可用this指针区分
在类的非静态成员函数中返回对象本身,可使用return *this;
-
空指针访问成员函数
-
空指针可以调用成员函数,但是需要考虑this指针
Person *p = NULL;
-
调用的成员函数中如果用到属性值,属性前面会默认有一个this->属性,如果创建的对象指针是空,则相当于NULL->属性,所以报错。
-
-
const修饰成员函数
-
常函数:
- 成员函数后加const我们称这个函数为常函数
-
常函数内不可以修饰成员属性
-
成员属性声明时加关键字mutable后,在常函数中依然可以修改
-
-
常对象
-
声明对象前加const称该对象为常对象
-
常对象只能调用常函数
-
-
-
友元
-
目的:让一个函数或者类访问另一个类的私有成员
-
关键字:friend
-
三种实现
-
全局函数做友元—将全局函数声明写在类内(可以是在上面),前面加上friend;
-
类做友元 --将(class
类名)写在类内(可以是在上面),前面加上friend; -
成员函数做友元–将全局函数声明写在类内,前面加上friend;必须声明是哪个类的函数;
-
-
-
运算符重载
-
加号运算符重载 operator+
就是C++编译器将你自己写的自定义数据类型的加法的名称统一命名为operator+,所以写运算符重载的时候只需要将加法函数的名称改为operator+,其他不变;
-
左移运算符 << 输出自定义类型 (链式编程)
注意:利用成员函数进行左移运算符重载,p.operator<<(cout) 简化版本
p<<cout;因此不会利用成员函数重载<<运算符,没法实现cout在左侧;只能利用全局函数重载左移运算符。 -
递增运算符
- 前置++运算符
返回值是引用类型是为了保证每次++操作的都是同一对象,如果返回值是值,那么返回的是新创建的临时变量,多次++后,操作的对象不是源对象本身,再次输入源对象后只进行了一次++操作。
-
后置++运算符
int 是占位参数,为了区分前置++函数。返回类型是值,因为不能返回局部变量。
-
赋值运算符重载
-
编译器提供的赋值运算符是浅拷贝,如果有成员指针在堆区分配了内存,则释放过程会报错;
-
因此需要自己编写深拷贝函数。
-
为了实现连等操作(链式编程),需要返回值为引用类型。
-
-
关系运算符
-
函数调用运算符重载 ()
-
函数调用运算符()也可以重载
-
由于重载后使用的方式非常像函数的调用,因此称为仿函数;
-
仿函数没有固定写法,非常灵活
-
-
-
继承
-
好处:减少重复代码
-
语法:class 子类(派生类): 继承方式 父类(基类)
-
继承方式:公共继承、保护继承、私有继承
-
继承中的对象模型
-
父类中所有的非静态成员属性都会被子类继承下去
-
父类中私有成员属性
被C++编译器给隐藏了,因此访问不到,但是确实被继承下去了 -
利用开发人员命令提示工具查看对象模型
跳转盘符 F: 跳转文件路径: cd 具体路径 查看类的结构图: cl /d1 reportSingleClassLayout类名 文件名
-
-
继承中的构造和析构顺序
-
问题:子类继承父类后,当创建子类对象,也会调用父类的构造函数,那么父类和子类的构造和析构函数的顺序是什么?
先构造父类,在构造子类,析构顺序与构造相反;
eg:父亲构造->子类构造->子类析构->父亲析构
-
-
继承同名成员处理方式
-
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或者父类中同名的数据呢?
-
访问子类同名成员,直接访问即可
-
访问父类同名成员,需要加作用域
-
注意:如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏父类的所有同名成员函数。如果想要访问父类的隐藏同名函数,需要加作用域。
-
-
-
继承同名静态成员处理方式
-
通过对象访问:和非静态成员方式一样
-
通过类名访问:直接加作用域 或者 Son::Base::m_A
-
-
多继承
-
允许多继承
-
语法:class 子类 : 继承方式 父类1 , 继承方式 父类2 , 继承方式
父类3…… -
多继承可能会引发父类中有同名成员出现,需要加作用域区分,因此实际上不建议使用
-
-
菱形继承
-
两个派生类继承同一个基类
-
又有某个类同时继承两个派生类
-
注意:新的类继承了两个派生类中来自基类的相同属性数据,导致新的类继承了两份相同的属性(来自基类),资源浪费。
两个m_Age,直接访问编译器不知道访问的哪个?需要加作用域。
-
解决方法:利用虚继承,继承之前,加上virtual。–基类称为虚基类
两个vaptr,指向同一个m_Age;
-
-
-
多态
-
分类:
-
静态多态:函数重载和运算符重载属于静态多态,复用函数名
-
动态多态:派生类和虚函数实现运行时多态
-
-
区别:静态多态的函数地址早绑定–编译阶段确定函数地址
动态多态的函数地址晚绑定–运行阶段确定函数地址
-
动态多态满足的条件
-
有继承关系
-
子类重写父类虚函数
-
使用的时候父类的指针或引用指向子类对象
函数前面声明virtual,表明是一个虚函数,这时类中会有一个vfptr指针,记录虚函数的地址。如果该类被继承,vfptr也会被继承,记录的地址也会被继承,在子类中重写虚函数的话,vfptr指针会指向子类的虚函数。因此当父类的指针或引用指向子类对象时,调用的时子类的虚函数,发生多态。如果父类的函数不是虚函数,则没有相应的指针记录,当父类的指针或引用指向子类对象时,调用的时父类的函数。子类和父类的赋值不需要强制转换。
-
-
纯虚函数和抽象类
-
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
-
因此可以将虚函数改为纯虚函数
-
语法:virtual 返回值类型 函数名 (参数列表) = 0;
-
当类中有了纯虚函数,这个类也称为抽象类;
-
抽象类特点:无法实例化对象,子类必须重写抽象类中的纯虚函数,否则也属于抽象类
-
-
虚析构和纯虚析构
-
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
-
解决方式:将父类的析构函数改为虚析构和纯虚析构
-
虚析构和纯虚析构共性
-
可以解决父类指针释放子类对象
-
都需要具体的函数实现(一个在内部实现,一个在外部实现)
-
-
虚析构和纯虚析构区别
- 如果时纯虚析构,该类属于抽象类,无法实例化对象
-
-
-
7. 文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件\<fstream \>
1. 文件类型
1. 文本文件---文件以文本的ASCII码形式存储在计算机中
2. 二进制文件---文件以文本的二进制形式存储在计算机中,用户不能直接读懂
2. 操作文件的三大类
1. ofstream:写操作
2. ifstream:读操作
3. fstream:读写操作
3. 写文件步骤(文本文件):
1. 包含头文件:\#include\<fstream\>
2. 创建流对象:ofstream ofs;
3. 打开文件:ofs.open("文件路径",打开方式);
4. 写数据:ofs\<\<"写入的数据";
5. 关闭文件:ofs.close;
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201001180635872.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNjI5Mg==,size_16,color_FFFFFF,t_70#pic_center)
文件打开方式可以配合使用,利用\|操作符
4. 读文件步骤(文本文件):
1. 包含头文件:\#include\<fstream\>
2. 创建流对象:ifstream ifs;
3. 打开文件:ifs.open("文件路径",打开方式);
4. 判断文件是否打开:ifs.is_open()函数用来判断
5. 读数据:
char buf[1024] = {0};
ifs \>\> buf; //第一种方法 按行
ifs.getline(buf,sizeof(buf)); //第二种方法 按行
string buf;
getline(ifs,buf); //第三种方法 按行
char c;
c = ifs.get(); //按字符
5. 关闭文件:ifs.close;
6. 写文件步骤(二进制文件):
1. 打开方式指定 ios::binary
2. 二进制方式写文件主要利用流对象调用成员函数write
3. 函数原型:ostream& write(const char\* buffer,int len)
4. 参数解释:字符指针buffer指向内存中的一段存储空间。len是读写的字节数。
7. 读文件步骤(二进制文件):
1. 二进制方式写文件主要利用流对象调用成员函数read
2. 函数原型:ostream& read(const char\* buffer,int len)
3. 参数解释:字符指针buffer指向内存中的一段存储空间。len是读写的字节数。