学习C++这一篇就够了(进阶篇)
内存模型
C++在执行程序的时候,将内存方向划分为4个区域:
-
代码区:存放二进制代码,由操作系统进行管理
-
全局区:存放全局变量、静态变量、常量,程序结束后由操作系统释放
-
栈区:存放函数参数、局部变量,由编译器自动分配和释放
-
堆区:由开发者申请分配和释放,若程序员不释放,程序结束由操作系统自动回收
意义:对于不同区域存放的数据,赋予不同的生命周期,给编程更大的灵活性。
代码区
存放CPU执行的二进制代码(机器指令)
特点:
-
共享:对于频繁被执行的程序,只需要在内存中有一份就够了
-
只读:防止被意外修改
全局区
-
存放全局变量和静态变量,还存放常量,包括字符串常量和其他常量
-
数据在程序结束后由操作系统进行释放
栈区
-
存放函数参数、局部变量
-
不要返回局部变量的地址,因为函数一执行完,栈区数据就被释放了,虽然编译器会做短暂的保留
堆区
-
这是由开发者分配和释放的,如果程序结束开发者不释放,也会操作系统回收
-
在C++中主要用new开辟堆区空间,用delete释放
new 和 delete
new作用:用于让开发者在堆区中开辟数据
delete作用:让开发者手动释放堆区数据
语法:
new 数据类型
delete 堆区地址
示例:
引用
作用:给变量起别名
语法:
数据类型 &别名 = 原名
注意事项:
-
引用必须初始化
-
初始化后,就不可以再发生改变了
-
引用必须引一块合法的内存空间,可以是栈区,可以是堆区,但不可以是自变量(比如数字)
示例:
引用做函数参数
作用:可以让形参修饰实参,代替指针中形参修改实参的操作
示例:
引用做函数返回值
作用:可以作为函数返回值类型返回
注意事项:不要返回局部变量的引用,函数执行完局部变量内存就被释放了,返回个锤子
示例:
引用的本质
本质:引用的本质在C++内部实现,它就是一个指针常量,由编译器内部转换
作用:也就说明为什么引用初始化之后就不可更改,因为指针指向不可改
& <—等于—> int* const
示例:
常量引用
作用:主要用来修饰形参,防止误操作
用法:在函数形参列表中,加const修饰形参,防止形参改变实参
示例:
函数进阶用法
函数的默认参数
在C++中,函数的形参列表中形参是可以用默认参数的
语法:
返回值类型 函数名 (参数 = 默认值)
{
}
注意事项:
-
如果函数某个参数有默认值,那么从这个位置之后的参数必须有默认值
-
如果调用的时候有实参,那就用实参,没有实参,就用默认值
-
如果函数声明有默认值,那么在函数定义的时候就不能有默认值
示例:
函数的占位参数
作用:用来给函数的参数列表中做占位,调用函数的时候填补该位置就行了
语法:
返回值类型 函数名(数据类型)
{
}
缺点:现阶段函数的占位函数存在意义不大。
示例:
函数重载
作用:函数名相同,其他的可以不同,可以提高函数的复用性
满足条件:
-
同一个作用域下
-
函数名称相同
-
函数参数类型不同,或者个数不同,或者顺序不同
注意事项:
-
函数的返回值不能作为函数重载的满足条件
-
具体调用的是哪一个函数,就看参数,看实参是否对应形参,比如类型、个数、顺序
示例:
引用作为函数重载
当引用作为函数参数时:
-
实参必须是一块合法的内存
-
如果实参不是内存,只是一个自变量,那么形参就必须加const来修饰
示例:
遇到默认参数
-
当函数重载遇到默认参数时,会出现二义性,也就是出错
-
使用时尽量避免出现默认参数
示例:
类与对象
-
C++本身就是面向对象的编程语言
-
面向对象三大特性:封装、继承、多态
-
C++中万物皆可为对象,对象上有属性和行为
-
具有相同性质的对象,称之为类
示例:
人可以作为对象,属性有姓名、年龄、身高···,行为有唱,跳、rap···
你和你的死党,是同一性质,属于人类;
车可以作为对象,属性有轮胎、车灯、方向盘···,行为有载人、音乐、显摆···
五菱与奥迪,是同一性质,属于车类;
封装
封装的意义
封装是C++面向对象三大特性之一
封装的意义:
-
将属性和行为作为一个整体,来表现生活中的事物
-
将属性和行为用权限加以控制
封装的术语:
-
类中的属性和行为,统称为成员
-
属性也叫成员属性或者成员变量
-
行为也叫成员函数或者成员方法
封装意义一:将属性和行为作为一个整体
语法:
class 类名{ 访问权限:属性/行为 }
示例:创建一个类为圆,那半径就是它的属性了。
封装意义二:将属性和行为用访问权限来加以管理
访问权限有三种:
-
public:公共权限,成员类内和类外都可以访问
-
protected:保护权限,成员类内可以访问,,类外不可以访问
-
private:私有权限,成员类内可以访问,类外不可以访问
protecred保护权限和private私有权限的区别在于后面要说到的继承上,前者子类可以访问父类,后者子类不可访问父类,这是后面的内容了。
示例:
struct和class
struct和class都可以表示一个类,区别在于两者默认的权限不同:
-
struct:默认权限为公共
-
class:默认权限为私有
成员属性设置为私有
优点:
-
所有成员设置成私有,自己可以控制读写权限
-
对于写权限,可以检查其数据的有效性
示例:所有成员设置成私有,自己可以控制读写权限
示例:对于写权限,可以检查其数据的有效性
C++中,初始化和清理是非常重要的安全问题:
-
构造函数:在创建对象时为对象成员属性初始化,该函数由编译器自动调用
-
析构函数:在对象销毁前系统自动调用,执行一些清理工作
完成对象的初始化和清理工作是编译器必须要我们做的事情,这两个函数由编译器自动调用,但是如果我们不写构造函数和析构函数,编译器就会自己去实现,只不过函数里面是空的,简称空实现
构造函数
语法:
类名(){}
特点:
-
不用写void,没有返回值
-
函数名与类名相同
-
可以有参数,因为可以发生函数重载
-
在调用对象时编译器自动调用,而且只会调用一次
析构函数
语法:
~类名(){}
特点:
-
不用写void,没有返回值
-
函数名与类名相同,且在前面加~
-
不可以有参数,因此不可以发生函数重载
-
在对象销毁前编译器自动调用,而且只调用一次
示例:
构造函数的分类和调用
两种分类方式:
-
按参数分:有参构造和无参构造(默认构造)
-
按类型分:普通构造和拷贝构造
三种调用方式:
-
括号法
-
显示法
-
隐式转换法
示例:
说说拷贝构造函数
所谓拷贝构造函数就是将一个创建完毕对象的属性默认赋值给新的对象,这个赋值过程由编译器自动完成
通常以下三种情况会调用到拷贝构造:
-
使用一个已经创建完毕的对象来初始化一个新对象
-
值传递的方式给函数参数传参
-
以值传递的方式返回局部对象
示例:
构造函数的调用规则
(1)默认情况下,创建一个类编译器至少会添加3个函数
-
默认构造函数(无参,函数体为空)
-
默认析构函数(无参,函数体为空)
-
默认拷贝函数,对属性进行默认拷贝
(2)如果开发者写了有参构造函数,编译器不再默认提供无参构造,但是会提供默认拷贝构造
(3)如果开发者写了拷贝构造函数,编译器不再提供其他构造函数,无参和有参都没有
浅拷贝和深拷贝
这是经典面试题经常出现的案例,是一个常见的坑
浅拷贝:就是简单的赋值拷贝
深拷贝:在堆区重新开辟一片内存,进行拷贝操作
注意事项:
-
如果涉及到空间开辟和释放,浅拷贝容易发生内存重复释放的非法操作
-
需要自己写一个拷贝构造函数,利用深拷贝去解决浅拷贝带来的内存释放问题
初始化列表
作用:用来初始化属性,一般是在构造函数中初始化
语法:
构造函数():属性(初值),属性(初值),属性(初值) { }
示例:
类对象作为类成员
类中的成员可以是一个其他类的对象,该成员就是对象成员
注意事项:
-
创建此类对象的时候,先构造对象成员,再构造自身
-
先析构自身,再析构对象成员
示例:
静态成员
在成员变量和成员函数前面加一个关键字:static,就成了静态成员
分类:
静态成员变量:
-
所有对象共享一份同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化
静态成员函数:
-
所有对象共享同一个函数
-
静态成员函数只能访问静态成员变量
根据上面三个特点,静态成员(变量或者函数)不属于某一个对象,所以:
-
public权限的既可以通过对象进行访问,也可以通过类名进行访问
-
private无论如何类外访问不了
示例:
C++的对象模型
空对象
-
空对象占用1个字节的内存空间
-
每个空对象内存地址独一无二
原因:C++编译器为了区分空对象的所占内存的位置
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数是分开存储的:
-
非静态成员变量,属于类的对象上的
-
静态成员变量,不属于类的对象上的,不占对象空间
-
成员函数,不属于类的对象上的,只产生一份函数实例,不占对象空间
示例:
this指针概念
每一个成员函数只会诞生一份函数实例,说明会有多个对象同时调用同一份函数的情况存在。
用来区分究竟是哪一个对象调用的函数,用的是this指针。
-
this指针是隐藏在每一个成员函数内部的一种特供的指针
-
不需要被定义,本来就是,直接使用即可
-
哪个对象调用了函数,其this指针就指向哪个对象
作用:
-
当形参和成员变量同名时,可用this指针来区分(当然最好还是编程规范)
-
在返回对象本身时,可以用return *this
示例:
空指针访问成员函数
C++中空指针是可以访问成员函数的,主要是要注意有没有用到this指针
-
成员函数中没有用到this指针,空指针可以调用成员函数;
-
成员函数中用到了this指针,空指针就不可以调用成员函数
-
为了保持代码的健壮性,一般会在函数中加入判断 if(this == NULL){ return;}
示例:
const修饰成员函数
常函数:
-
成员函数后面加const修饰,称之为常函数
-
不可以修改成员属性
-
如果成员属性在声明时加了关键字mutable ,在常函数中就可以修改
常对象:
-
声明对象加const修饰,称之为常对象
-
不可以修改成员属性
-
常对象只能调用常函数,因为普通函数可以修改成员属性
示例:
友元
作用:让一个函数或者类访问另一个类中的私有成员
关键字:friend
3种实现方式:
-
全局函数做友元
-
类做友元
-
成员函数做友元
示例:
运算符重载
作用:对已经有的运算符重新进行定义,赋予其另外一种功能,适应不同的自定义数据类型
关键字:operator
-
加号运算符重载
-
左移运算符重载
-
递增运算符重载
-
赋值运算符重载
-
关系运算符重载
-
函数调用运算符重载
运算符重载的方式有2种:
-
成员函数重载
-
全局函数重载
温馨提示:运算符重载也可发生函数重载(函数名相同,函数参数不同)
示例:
继承
-
继承是面向对象三大特性之一
-
有些类与类之前有些特殊的关系,用的就是继承技术,减少重复代码
-
下一级别类除了拥有上一级别类的共性外,还有自己的特性
语法:
class 子类 : 继承方式 父类
子类,也叫派生类;
父类,也叫基类
示例:
class 类名 :public 类名
{
}
继承方式
-
公共继承
-
保护继承
-
私有继承
公共继承:父类私有权限变量不可访问,公共权限和保护权限变量照常继承;
保护继承:父类私有权限变量不可访问,公共权限和保护权限变量变成自己的保护权限变量;
私有权限:父类私有权限变量不可访问,公共权限和保护权限变量变成自己的私有权限变量;
示例:
对象模型
-
父类中所有非静态成员属性都会被子类继承下去
-
父类中的private也会被继承,虽然子类访问不到,那是因为编译器隐藏起来了
-
利用”开发人员命令提示工具“可查看子类继承后的对象模型
工具用法:
-
打开VS软件下的”开发人员命令提示工具“
-
跳转到当前子类所在的文件路径下
-
查看命令:cl /dl reportSingleClassLayout类名 文件名
构造函数和析构函数
子类继承父类中,构造函数和析构函数的顺序:
-
父类 的 构造
-
子类 的 构造
-
子类 的 析构
-
父亲 的 析构
同名成员/函数处理方式
-
访问子类同名成员(变量和函数),直接访问
-
访问父类同名成员(变量和函数),需要加作用域
-
函数重载也一样,就算参数不同也属于同名函数
示例:
静态同名成员
-
与非静态成员处理方式一致(同上)
-
访问子类同名成员,直接访问
-
访问父类同名成员,需要加作用域
多继承
-
C++中允许一个类继承多个父类
-
多继承也会引发父类中同名成员出现,也需要加作用域
-
实际开发中不建议用多继承,容易出现太多二义性
语法:
Class 子类 : 继承方式 父类1 , 继承方式 父类2
菱形继承
-
两个派生类继承同一个基类
-
同时又有某一个类同时继承两个派生类
-
这种继承就被成为菱形继承,或者钻石继承
-
当出现菱形继承时,某一个类就拥有了两份基类的相同数据,需要用作用域加以区分
-
但是我们其实只需要一份数据就够了,多出来的数据纯属就是浪费,用虚继承方式可以解决
-
因为不建议用多继承方式,所以也不建议写菱形继承
-
可以用”开发人员命令提示工具“查看其对象模型
示例:
虚继承
关键字:virtual
-
利用虚继承可以解决菱形继承出现的二义性问题
-
在继承之前加上关键词virtual,就变成了虚继承
-
虚继承以最新修改的成员数据为准
-
多份相同数据的来源的那个基类,称之为虚基类
-
因为不建议用多继承方式,所以也不建议写菱形继承
示例:
多态
多态是C++面向对象三大特性之一
多态的分类:
-
静态多态:其函数的地址在编译阶段确定,比如 函数重载 和 运算符重载
-
动态多态:其函数的地址在运行阶段确定,比如 派生类 和 虚函数实现运行
动态多态的满足条件:
-
有继承关系
-
子类重写父类的虚函数
-
用父类的指针或引用来执行子类对象
纯虚函数
-
在多态中,父类中的虚函数通常没有任何意义,因为调用的都是子类重写的函数
-
可以将父类中的虚函数改为 纯虚函数
-
当一个类中有了纯虚函数,这个类被成为抽象类
语法:
virtual 返回值类型 函数名(参数列表)= 0 ;
特点:
-
无法实例化对象
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
问题:在使用多态时,如果子类中开辟了堆空间,父类指针在释放时无法调用子类的析构函数
解决办法:将父类中的析构函数改成 虚析构 或者 纯虚析构
二者的共性:
-
可以帮助父类指针释放子类对象
-
都需要有具体的函数实现
-
如果子类中没有开辟堆区,就可以不写虚析构 或者 纯虚析构
-
一个类中如果用于纯虚析构,这个类也被成为抽象类
二者的区别:
-
纯虚析构所在的类属于抽象类,无法实例化对象
-
纯虚析构除了需要声明,也还需要实现
虚析构的语法:
virtual ~类名()
{
}
纯虚析构的语法:
virtual ~类名() = 0 ; //声明
类名::~类名() //实现
{
}
文件操作
-
程序运行时产生的数据都是临时数据,程序一旦执行完毕数据都会被释放
-
C++提供一个文件流的操作,通过文件来让数据持久化
-
文件操作需要的头文件<fstream>
文件类型的分类:
-
文本文件:文件以ASCII的形式存储
-
二进制文件:文件以二进制的形式存
文件操作的分类:
-
ofstream:写操作
-
ifstream:读操作
-
fstream:读写操作
文件打开方式:(多种打开方式用位或操作符 “ | ”隔开即可)
-
ios::in (为读文件而打开)
-
ios::out (为写文件而打开)
-
ios::ate (初始位置:文件尾部)
-
ios::app (以追加方式写文件)
-
ios::trunc (如果文件已经存在,先删除再创建)
-
ios::binary (以二进制方式打开文件)
文本文件
-
写文件
-
包含头文件:#include<fstream>
-
创建文件流对象:ofstream ofs
-
打开文件:ofs.open("文件路径", "打开方式")
-
写数据:ofs << "写入的数据"
-
关闭文件:ofs.close()
示例:
-
读文件
-
包含头文件:#include<fstream>
-
创建文件流对象:ifstream ifs
-
打开文件并判断打开是否成功:ifs.open("文件路径", "打开方式") if( !ofs.is_open() ) { }
-
读数据:有四种读取方式
-
关闭文件:ofs.close()
示例:
二进制文件
-
二进制文件比较强大,除了可以处理内置数据类型(int,char,double),还可以处理自定义数据类型
-
打开方式要额外指定:ios::binary
-
写文件方式主要利用流对象调用成员函数 write()
-
函数原型 : ostream& write ( const char * buffer, int len )
-
读文件方式主要利用流对象调用成员函数 read ()
-
函数原型:istream& read ( char * buffer, int len )
-
写文件
-
包含头文件:include<fstream>
-
创建流对象: ofstream ofs
-
打开文件并判断是否打开成功:ofs.open("文件路径", "打开方式")
-
读文件:创建类对象p,ofs.write( (const char*)&p , sizeof(类) )
-
关闭文件:ofs.close()
示例:
-
读文件
-
包含头文件:include<fstream>
-
创建流对象: ifstream ifs
-
打开文件并判断是否打开成功:ifs.open("文件路径", "打开方式") if( !ifs.is_open() )
-
读文件:创建类对象p,ifs.read( (char*)&p , sizeof(类) )
-
关闭文件:ifs.close()
示例:
先更新到这儿吧,需要后面在补充。
希望以上内容可以帮助到大家。
祝各位生活愉快。