• C++核心编程

    本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。

    1.内存分区模型

    C++程序在执行时,将内存大方向划分为4个区域

    内存分区的意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程性

    1.1程序运行前

    在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

    代码区

    存放CPU执行的机器指令

    代码是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

    代码是只读的,使其只读的原因是防止程序意外的修改了它的指令

    全局区

    全局变量、静态变量、全局常量、字符串常量存放在全局区

    局部变量、局部常量不在全局区

    该区域的数据在程序结束后由操作系统释放

    static 修饰的叫静态,const 修饰的叫常量

    1.2程序运行后

    栈区

    由编译器自动分配释放,存放函数的参数值(形参)和局部变量

    注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

    堆区

    由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

    1.3new操作符

    C++中利用new操作符在堆区开辟数据

    利用new创建的数据,会返回该数据对应的类型的指针

    基本语法如下:

    2.引用

    2.1引用的基本使用

    作用:给变量起别名

    语法:数据类型 &别名 = 原名;

    示例如下:

    2.2引用的注意事项

    1. 引用必须初始化
    2. 引用在初始化后不允许更改

    2.3引用做函数参数

    以前学过两种交换函数的写法,分别是值传递和地址传递,但是值传递仅仅是形参的交换,它无法影响实参,故使用地址传递,现在用引用的方法也可以实现和地址传递同样的效果。示例如下:

    2.4引用做函数的返回值

    1.不要返回局部变量的引用。

    2.函数调用可作为左值。

    示例如下:

    2.5引用的本质

    本质:引用的本质在C++内部实现是一个指针常量

    结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

    2.6常量引用

    作用:常量引用主要用于修饰形参,防止误操作

    在函数形参列表中,可以加const修饰形参,防止形参改变实参

    示例如下:

    3.函数提高

    3.1函数的默认参数

    在C++中,函数的形参列表的形参是可以有默认值的。

    语法:返回值类型 函数名 (参数 = 默认值) {}

    如果我们自己传入数据,就用我们自己的数据,否则就用默认值

    注意事项:

    1.如果某个位置参数有默认值,那么从这个位置往后,从左到右,必须都要有默认值

    2.如果函数声明有默认值,函数实现的时候就不能有默认参数,声明和实现只能有一个有默认参数

    3.2函数占用参数

    C++中函数的形参列表中可以有占位参数,用来作占位,调用函数时必须填补该位置

    语法:返回值类型 函数名 (数据类型) {}

    在现阶段中占位参数存在的意义不大,但是在后面的学习中会用到该技术

    示例如下:

    3.3函数的重载

    3.3.1函数重载概述

    作用:函数名可以相同,提高复用性

    函数重载满足条件:

    注意:函数的返回值不可以作为函数重载的条件

    示例如下:

    3.3.2函数重载的注意事项

    示例如下:

    4.类和对象

    C++面向对象的三大特性为:封装、继承、多态

    C++认为万事万物皆为对象,对象有其属性和行为

    4.1封装

    4.1.1封装的意义

    封装是C++面向对象三大特性之一,封装的意义:

    语法为:class 类名{访问权限:属性/行为}

    示例:写一个圆类,并求出圆的周长

    示例:创建一个学生类

    访问权限有三种:

    1. public 公共权限 类内可以访问,类外也可以访问
    2. protected 保护权限 类内可以访问,类外不可以访问 儿子可以访问父亲中的保护内容
    3. private 私有权限 类内可以访问,类外不可以访问 儿子不可以访问父亲中的私有内容

    示例如下:

    不论是属性还是行为,都要遵守权限规则,定义在protectedprivate下的行为(函数/方法)同样无法在类外访问到。

    4.1.2 struct和class的区别

    在C++中structclass的唯一区别就在于默认的访问权限不同

    struct的默认权限为公共,class的默认权限为私有

    4.1.3成员属性设置为私有

    优点:

    1. 将所有成员属性设置为私有,可以自己控制读写权限
    2. 对于写权限,我们可以检测数据的有效性

    示例如下:

    4.2对象的初始化和清理

    4.2.1构造函数和析构函数

    对象的初始化清理是两个非常重要的安全问题

    一个对象或者变量没有初始状态,对其使用的后果是未知的。

    同样的使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题。

    C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,并且编译器提供的构造函数和析构函数是空实现。

    构造函数语法类名 () {}

    1. 构造函数,没有返回值也不写void
    2. 函数名称与类名相同
    3. 构造函数可以有参数,因此可以发生重载
    4. 程序在调用对象时会自动调用构造,无需手动调用,且只调用一次

    析构函数语法~类名 () {}

    1. 析构函数,没有返回值也不写void
    2. 函数名称与类名相同,在名称前加上符号~
    3. 析构函数不可以有参数,因此不可以发生重载
    4. 程序在对象销毁前自动调用析构,无需手动调用,且只调用一次

    4.2.2构造函数的分类及调用

    两种分类方式:

    按参数分为:有参构造和无参构造

    按类型分为:普通构造和拷贝构造

    三种调用方式: 括号法、显示法、隐式转换法

    示例如下:

    括号法注意事项: Person p1(); 不要这样写,加了括号,编译器会认为这行代码是一个函数声明,而不是一个对象的创建。

    显示法注意事项:

    1.单独拿出等号右侧,Person (10),这是一个匿名对象,它在执行完当前行后,系统会立即回收掉匿名对象。

    2.不要利用拷贝构造函数,初始化匿名对象。Person (p3); 编译器会认为它等价于 Person p3,这样的话,由于上面也定义过p3,就会出现重定义的报错!

    4.2.3拷贝构造函数调用时机

    C++中拷贝构造函数调用时机通常有三种情况

    示例如下:

    4.2.4构造函数调用规则

    默认情况下,C++编译器至少给一个类添加3个函数

    1. 默认构造函数(无参,函数体为空)
    2. 默认析构函数(无参,函数体为空)
    3. 默认拷贝构造函数,对属性进行值拷贝

    构造函数调用规则如下:

    4.2.5深拷贝和浅拷贝

    浅拷贝:简单的赋值拷贝操作

    深拷贝:在堆区重新申请空间进行拷贝操作

    示例如下:

    总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来内存空间重复释放的问题。

    4.2.6初始化列表

    作用:C++提供了初始化列表语法,用来初始化属性

    语法为:构造函数():属性1(值1),属性2(值2),属性3(值3)...{}

    示例如下:

    4.2.7类对象作为类成员

    C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。

    当B类中有对象A作为成员,A和B的构造和析构的顺序谁先谁后?

    构造顺序为:A先,B后

    析构顺序为:B先,A后

    4.2.8静态成员

    静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。

    静态成员分为:

    示例1:静态成员变量

    示例2:静态成员函数

    4.3C++对象模型和this指针

    4.3.1成员变量和成员函数分开存储

    在C++中,类内的成员变量和成员函数分开存储

    只有非静态成员变量才属于类的对象上,静态成员变量、非静态成员函数、静态成员函数都不属于类的对象上

    空对象占用的内存空间为1个字节,C++编译器会给每个空对象分配1个字节空间,是为了区分空对象占内存的位置,每个空对象应该有一个独一无二的内存地址。

    4.3.2this指针的概念

    每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

    那么问题是:这一块代码是如何区分是哪个对象调用自己呢?

    C++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

    this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可。

    this指针的用途

    示例如下:

    4.3.3空指针访问成员函数

    C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

    如果用到this指针,需要加以判断保证代码的健壮性

    示例如下:

    4.3.4const修饰成员函数

    常函数:

    常对象:

    示例如下:

    this指针的本质是一个指针常量,它的指向是不可以修改的,在成员函数后加const后,限定指针指向和指向的值都不可以修改了

    4.4友元

    生活中你的家有客厅(public),有你的卧室(private)。客厅所有的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的好闺蜜好基友进去。

    在程序里,有些私有属性也想让类外特殊的一些函数或者类访问,就需要用到友元技术,友元的目的就是让一个函数或者类访问另一个类中的私有成员

    友元的关键字为friend

    友元的三中实现

    4.4.1全局函数做友元

    示例如下:

    4.4.2类做友元

    示例如下:

    也可以将成员函数写在类外面,比如上面示例中 goodgay 类中的 visit() 成员函数。

    4.4.3成员函数做友元

    与全局函数、类做友元类似。核心代码如下:

    4.5运算符重载

    运算符重载的概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

    4.5.1加号运算符重载

    作用:实现两个自定义数据类型的相加运算

    示例1:成员函数实现 + 号运算符重载

    示例2:全局函数实现 + 号运算符重载

    示例3:运算符重载也可以发生函数重载,以下以实现Person + int为例

    注意:1.对于内置的数据类型的表达式的运算符是不能改变的。2.不要滥用运算符重载。

    4.5.2左移运算符重载

    作用:剖析cout的本质,可以输出自定义的数据类型。

    示例如下:

    总结:有些时候成员变量属性是private,这时重载左移运算符配合友元可以实现输出自定义数据类型。

    4.5.3递增运算符重载

    作用:通过重载递增运算符,实现自己的整型数据。

    示例如下:

    注意:重载前置++返回为引用,重载后置++返回为,而且重载后置++需要用int做占位参数

    4.5.4赋值运算符重载

    C++编译器至少给一个类添加4个函数

    1. 默认构造函数(无参,函数体为空)
    2. 默认析构函数(无参,函数体为空)
    3. 默认拷贝构造函数,对属性进行值拷贝
    4. 赋值运算符 operator=,对属性进行值拷贝

    如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝的问题,即堆区内存重复释放。

    示例如下:

    4.5.5关系运算符重载

    作用:重载关系运算符,可以让两个自定义类型的对象进行对比操作

    示例如下:

    4.5.6函数调用运算符重载

    示例如下:

    4.6继承

    继承是面向对象三大特性之一

    4.6.1继承的基本语法

    继承的好处:减少重复的代码

    语法:class 子类 : 继承方式 父类 {};

    子类也称派生类,父类也称基类。

    子类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过来的表现其共性,而自己新增的成员表现其个性。

    4.6.2继承方式

    继承的语法:class 子类 : 继承方式 父类 {};

    继承一共有3种方式:

    特点:这3种继承方式子类都不能访问到父类中的私有成员属性,公共继承,子类继承的父类的公共属性仍为公共属性、保护属性仍为保护属性;保护继承,子类继承的父类的公共属性变为保护属性、保护属性仍为保护属性;私有继承,子类继承的父类的公共属性、保护属性全部变为私有属性。

    公共属性在类内和类外都可以访问到;保护属性在类内可以访问、在类外不可以访问;私有属性在类内可以访问、在类外不可以访问。

    4.6.3继承中的对象模型

    问题:从父类继承过来的成员,哪些属于子类对象中?

    结论:父类中的私有成员也是被子类继承下去了,只是由于编译器给隐藏后访问不到。

    利用工具(开发人员命令提示符)查看的步骤:

    1. 在开始菜单找到VS开发人员命令提示符,打开。
    2. 打开工具窗口后,定位到当前cpp文件所在的盘符,然后cd cpp文件所在路径
    3. 输入 cl /d1 reportSingleClassLayout类名 所属文件名(这个可以输入部分cpp文件名,然后按Tab键补齐)
    4. 然后回车,即可看到类的对象模型

    4.6.4继承中构造和析构顺序

    子类继承父类后,当创建子类对象,也会调用父类的构造函数

    问题:父类和子类的构造和析构顺序是谁先谁后?

    结论:继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反,先析构子类,再析构父类。

    4.6.5继承同名成员处理方式

    问题:当子类和父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

    示例如下:

    总结:

    1. 子类对象可以直接访问到子类中的同名成员
    2. 子类对象加作用域可以访问到父类中的同名成员
    3. 当子类和父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数(包括重载的函数),如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域

    4.6.6继承同名静态成员处理方式

    问题:继承中同名的静态成员在子类对象上如何进行访问?

    静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问方式(通过对象和通过类名)

    要理解两个双冒号的意义不同,例如:

    4.6.7多继承语法

    C++允许一个类继承多个类

    语法:class 子类 :继承方式 父类1,继承方式 父类2 ...{};

    多继承可能会引发父类中有同名成员出现,需要加作用域区分,所以C++在实际开发中不建议用多继承。

    4.6.8菱形继承(钻石继承)

    菱形继承的概念:两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承就叫做菱形继承也叫钻石继承。

    示例如下:

    注意:虚继承后继承的是一个vbptr指针(v--virtual、b--base、prt--pointer),该指针指向虚继承表,表里面记录了到虚基类那份独一无二的成员属性的偏移量,这样就确保了孙子类里面的数据只有一份,直接访问也不会出现二义性。

    总结:

    4.7多态

    4.7.1多态的基本概念

    多态是C++面向对象三大特性之一

    多态分为两类

    静态多态和动态多态的区别:

    示例如下:

    总结:

    多态满足的条件

    多态的使用条件

    重写:函数返回值类型、函数名、参数列表完全一致称为重写,注意区分重载。

    4.7.2多态的优点

    多态的优点:

    C++开发提倡利用多态设计程序架构,因为多态优点很多。

    4.7.3纯虚函数和抽象类

    在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类中重写的内容,因此可以将虚函数改为纯虚函数

    纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;

    当类中有了纯虚函数,这个类也称为抽象类

    抽象类的特点:

    4.7.4虚析构和纯虚析构

    多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

    解决方式:将父类中的析构函数改为虚析构或者纯虚析构

    虚析构和纯虚析构的共性:

    虚析构与纯虚析构的区别:

    虚析构语法:virtual ~类名(){父类析构代码}

    纯虚析构语法:类内写声明virtual ~类名() = 0;类外写实现 类名::~类名(){父类析构代码}

    总结:

    1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
    2. 如果子类中没有堆区数据,可以不在父类中写虚析构或纯虚析构
    3. 拥有纯虚析构函数的类属于抽象类,不能实例化对象

    5.文件操作

    程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化。

    C++中对文件操作需要包含头文件<fstream>

    文件类型分为两种:

    1. 文本文件 文件以文本ASCLL码的形式存储在计算机中
    2. 二进制文件 文件以文本二进制的形式存储在计算机中,用户一般不能直接读懂它们

    操作文件的三大类:

    1. ofstream:写操作
    2. ifstream:读操作
    3. fstream:读写操作

    5.1文本文件

    5.1.1写文件

    写文件的步骤如下:

    1. 包含头文件,#include <fstream>
    2. 创建流对象,ofstream ofs;
    3. 打开文件,ofs.open("文件路径",打开方式);
    4. 写数据,ofs << "写入的数据" ;
    5. 关闭文件,ofs.close();

    常见的文件打开方式如下:

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

    注意:文件打开方式可以配合使用,利用|操作符。例如:用二进制的方式写文件 ios::binary | ios::out

    总结:

    5.1.2读文件

    读文件与写文件的步骤相似,但是读取方式相对比较多

    读文件步骤如下:

    1. 包含头文件,#include <fstream>
    2. 创建流对象,ifstream ifs;
    3. 打开文件并判断文件是否打开成功,ifs.open("文件路径",打开方式);
    4. 读数据,有四种方式读取
    5. 关闭文件,ifs.close();

    示例如下:

    总结:

    5.2二进制文件

    以二进制的方式对文件进行读写操作,打开方式要指定为 iOS::binary

    5.2.1写文件

    二进制方式写文件主要利用流对象调用成员函数write

    函数原型:ostream& write(const char * buffer,int len);

    参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

    示例如下:

    总结:文件输出流对象可以通过write函数,以二进制的方式写数据

    5.2.2读文件

    二进制方式读文件主要利用流对象调用成员函数read

    函数原型:istream& read(char *buffer,int len);

    参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

    示例如下:

    总结:文件输入流对象可以通过read函数,以二进制的方式读数据