C++基础
C++17
常用
1 | ①在方法或属性前加上类名:: 表示该方法或者属性作用域是这个类 |
前言
头文件
自定义头文件,往往以.h为后缀
1 | #include "iostream" //c++输入输出库 |
编译
概述:就是将cpp文件的代码编译成计算机可执行的exe文件。
编译器:mingw(即gcc在window平台的版本)
手动编译命令:g++ 文件名.cpp -o 文件名.exe
基础语法
输入和输出
cout
作用:帮助将内容打印到控制台上
使用:cout<<内容;
换行:<<endl;
输出多份内容:可以通过多个 << 进行自由组合输出内容。
中文输出乱码解决
方法一:
引入windows.h库,即#include “windows.h”
使用SetConsole0utputCP(“CP_UTF8”); //修改控制台输出编码格式
方法二:
直接添加system(“chcp 65001”); //表示将控制台输出编码改为65001也就是utf8
cin
作用:完成从键盘输入数据提供给运行的程序
语法:cin >> 变量; //输入数据对变量赋值
中文乱码解决:在clion中按下Ctrl + Alt + Shfit + / 四个键,选择注册表,把run那个取消掉
注释
单行注释:// 内容
多行注释:/* 内容 */
常量
概述:在程序执行过程中值不会改变的量。
常量类型的决定:在C++中,对于常量的类型确定遵循最小原则
按照所书写的字面量的值,来确定类型,默认int,从小到大进行适配
或按照指定后缀确定,后缀大小写都可以,常用的,U、L、UL、ULL(unsigned long long)、F、D(double)
字面常量
概述:被书写到代码内的常量称之为字面常量,也称字面量。
整型字面常量:整数,不带小数点的数字
实型字面常量:小数,带有小数点的数字
上述两种叫做数值型常量
字符型字面常量:以单引号’ ‘包围的,单个字符
字符串字面常量:以双引号” “包围的,任意个字符
符号常量
概述:使用标识符去定义的常量,称之为符号常量。简单来说,符号常量就是给常量起一个名字,就是符号常量。
定义语法:#define 标识符(名称) 常量 //这个#define是宏命令,标识符即符号常量的名称,建议全部大写
例如:#define FAT_ BMI 28 //肥胖BMI,因为经常使用这个常量,所以使用这种方法定义
注意:
①定义在代码的头部
②不需要添加分号结尾
③满足标识符的要求下,字母全部大写
标识符
概述:即表示某类实体的符号(名称)。
比如:符号常量名、变量名、类名称、文件名、方法/函数名等等,都是标识符。
内容限定:
①只允许字母、数字、下划线的组合,数字不可开头
②大小写敏感
③不可以是关键字
常用命名规范:
下划线命名法:字母一般全小写,单词之间用下划线隔开。一般应用于变量名
小驼峰命名法:首单词首字母小写,其他单词首字母大写。一般应用于函数名,变量名
大驼峰命名法:单词首字母全部大写。一般应用于类名
关键字
概述:在c++内具有特殊意义的单词
变量
概述:在程序运行时,能储存计算结果或能表示值的抽象概念。简单的说,变量就是在程序运行时,记录数据用的。
定义变量语法:变量类型 变量名;
变量赋值语法:变量名 = 变量值;
常见的变量类型:
1 | 支持一次性声明多个变量,例如: |
变量的作用域
{… …}称之为代码块,也是内部变量的作用域,在{}内部声明的变量,只能在内部使用
作用域可以向内层的{}进行传递,但是外部是无法访问到的
数据类型
int
int即整型,整型分为4种如下:
可以使用sizeof(变量名)查看占用空间大小
无符号和有符号数字
无符号:仅仅允许正数存在
有符号:可以允许负数存在
上述图中的[ ]表示可选,且默认时就代表有符号
无符号的快捷写法:
u_int 变量名 = 0; //其他同理
float
float即实型。还分为3种
实型数据全部都是有符号的,并且不同编译器有效位不一样。
注意有效位数,超出了就会不准确,小数点也占一位
1 | 输出控制小数格式 |
char
char即字符型。
char型的本质是数字,在内存中是以数字存储的,以ASCII码表转换。
97是a,65是A。并且字符可以进行数字加减。
string
string即字符串型。
C语言风格的字符串,如下:
char a[] = “hello”; //字符数组形式,不可更改变量值
char *b = “hello”; // 指针形式
C++风格的字符串
string c = “hello”;
字符串拼接
方法一:可以直接使用 + 进行连接,但+号只适用于字符串和字符串的连接。可以使用to_string()内置函数将内容转换位字符串
boolean
布尔值只有true和fasle,本质是1和0
转义字符
\就是转义符号
ASCII表中字符分为两类:非打印控制字符、打印字符
非打印控制字符:无法打印出形态,但是可以用于表示各种控制功能,如换行、制表等
\t可以将前面的字符以空格填充的形式,补充至8个字符位
运算符
算数运算符
赋值运算符
概述:是一种双目运算符,用于将右侧表达式的值赋予左侧变量。
比较运算符
概述:是一种双目运算符,用于对两个数据进行比较(大、小、相等) ,得到bool型结果。
c语言风格的字符串,比较的是内存地址。可以使用cstring库中的strcmp(s1,s2)方法是比较两个字符串的内容,返回-1,0,1
c++风格的字符串可以直接比较内容
逻辑运算符
概述:是一种单/双目运算符,用于对单个或多个表达式进行逻辑判断。
三元运算符
概述:是一种三目运算符,用于对逻辑进行判断,根据判断结果提供不同值。
语法:产出bool结果的表达式 ? 值1 : 值2;
含义:如果?之前的表达式结果为true,那么提供值1的结果,如果结果为false,那么提供值2的结果
位运算符
判断语句
if
if-else
if-else if
控制语句
switch
格式:switch(表达式){
case 常量表达式1:
语句体1;
break;
case 常量表达式2:
语句体2;
break;
….
default:
语句体n+1;
break;
}
注意事项:
①default是前面所有结果不匹配后,就会进行这个,他可省略,并且可在任何位置。
②case后面只能是字面量或者枚举类型,不能是变量,且不能重复。
循环语句
for
while
do-while
枚举类型
概述:本质上是一个被命名的整型常数的集合。在C\C++语言中它是种基本数据类型 。
作用:提高代码的可读性、可维护性和键入性。因为枚举可以将一些数字或字符串符号化,以此增强程序的可读性和可维护性。
1 | 语法: |
中断语句
break
continue
goto
作用:提供无条件跳转功能
语法:
标记名字:
goto 标记名字;
数组
概述:是由一批相同类型的元素( eLement)的集合所组成的数据结构。分配一块连续的内存去存储
语法:数据类型 数组名[数组长度];
1 | 特点: |
静态赋值:int v[] = {1,2,3};
遍历:使用for或者while
字符数组
c语言风格定义的字符串,本质就是一个字符数组:
表现一:”he”
表现二:char s[] = “he”;
均可以表现为:char s[] = {‘h’,’e’, ‘\0’};
上述2种C语言风格字符串,其存储机制:将每一个字符,作为1个元素,存入字符数组中。在字符数组中,额外在最后添加一个元素\0 (空字符) ,作为结束标记。
注意:中文一定要使用string类型,不要用char数组。
多维数组
概述:对数组进行嵌套,即数组内的每个元素依旧是数组。
二维数组定义:int v[3][3];
1 | 遍历二维数组: |
数组元素的移除
前景:C++内置并未提供对数组元素进行增加(插入)、移除的功能,需要手动实现(vector容器提供)。
实现:
1.通过new操作符,申请新数组的内存空间,并复制数据到新数组
2.通过delete删除旧数组的空间占用
3.将旧数组指针,指向新数组地址
本质:是将需要的元素复制到了一个新的数组
指针
概述:特定类型数据在内存中的存储地址,即内存地址。指针只是一个逻辑概念,其实际应用是:指针变量
指针变量:即变量p存储了一个变量的地址。并且指针变量本身也有自己的存储地址
本质:协助程序员完成内存的直接操纵
指针也可以像数组那样使用索引,p[0] = *(p+0)
1 | 定义方法一:先声明,后赋值 |
指针运算
1 | 指针进行加减运算的结果,和指针指向内存区域的数据类型有关,以加法为例: |
指针悬挂
概述:指针指向区域已经被回收(deLete),这种问题称之为指针悬挂。
表现:指针悬挂之后,所指向的值是随机不确定的。容易造成内存安全问题。
解决方法:
不要轻易进行指针之间相互赋值
deLete回收空间前,确保此空间100%不再被使用
常量指针
被const修饰的指针就叫常量指针。
共分为已下3中类型的:
①指向const的指针:表示指向区域的数据,是不变的,但可以更换指向。
语法:const 数据类型 *指针; 或者 数据类型 const *指针;
只需确保const在*前面即可。
该类型的也就是只能指向常量也就是const
②const指针:表示指针本身不可更改,但指向的数据可以更改。
语法: 数据类型 * const 指针 = 地址; //必须初始化地址,因为地址不可修改
③指向const的const指针:指针和指向区域的值,都不可更改。
语法:const 数据类型 * const 指针 = 地址; //必须初始化地址,因为地址不可修改
使用场景:
①需要常量的同时也需要动态内存分配的场景
②因为只有使用指针,方可动态分配内存
动态内存分配
概述:即由程序员手动的进行内存空间的分配、内存空间的释放等内存管理操作。
前景:
C++代码中,变量、数组等对象的创建,是由C++自动分配内存的,称之为(自动)静态内存分配。
(自动)静态内存管理,是不会进行内存空间的自动清理的。 (无垃圾回收机制)。
我们需要手动的管理内存,即手动分配,用完清理。
new
概述:new运算符用于申请并分配内存空间,并提供指向该空间的指针(内存地址)
基本语法:
new type //申请普通变量空间
new type[n] //申请数组空间
delete
概述:delete运算符用于释放内存,仅可用于new运算符申请的内存区域
基本语法:
delete 指针 //删除普通变量空间
delete[] 指针 //删除数组空间
注意:常量是不需要删除的,main函数执行完会自动清理。
结构体
概述:结构体(struct) ,是一种用户自定义复合数据类型,可以包含不同类型的不同成员。
1 | 语法如下: |
结构体数组
作用:将自定义的结构体放入到数组中方便维护
创建:struct 结构体名 数组名[元素个数] = { {},{},…{} }
建议使用new进行动态赋值
结构体指针
作用:通过指针访问结构体中的成员
利用操作符 -> 可以通过结构体指针访问结构体属性
1 | 应用一:引入已经存在的结构体变量地址 |
结构体嵌套结构体
作用:结构体中的成员可以是另一个结构体
示例:
1 | 例如:每个老师辅导一个学员, 一个老师的结构体中,记录一个学生的结构体 |
结构体作为参数
作用:将结构体作为参数向函数中传递
传递方式有两种:
①值传递(不会影响原本的值)
②地址传递(会跟着形参的改变而改变) #定义的时候传入struct student *p,使用的时候传入&s1。用->访问内部属性
注意:使用地址传递,可以通过const防止内部属性得到修改。
函数
概述:是一个提前封装好的、可重复使用的、完成特定功能的独立代码单元。
定义同java一样
函数传参
值传递、地址传递、引用传递
值传递
值传递会复制值,无法对实参本身产生影响
地址传递
地址传递会复制地址,对实参本身可以产生影响,但指针写法麻烦
比如指针,数组等
传入数组一般建议附带数组长度的传入,因为C++不会检查数组的内存边界。
1 | 例如交换两个变量的值 |
引用传递
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
像普通变量那样操作,但对实参本身可以产生影响
使用&表示传递引用对象
1 | 例如交换两个变量的值 |
返回指针
定义要用指针类型接收,return返回一个指针。
但是要注意函数中的局部变量会在函数执行完自动清理。
需要返回数组时,使用指针接收也是一样的。但不能返回一个局部创建的静态数组。用new动态分配的可以返回,或者全局的静态数组。
函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名(参数 = 默认值) {}
注意事项:
如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
如果函数声明(即不带函数体的函数声明)有默认参数,函数实现就不能有默认参数。
函数的声明和实现只能有一个有默认参数
函数占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法:返回值类型 函数名(数据类型){}
即形参对应位置只填数据类型,占位参数也可以有默认值。
函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
●同一个作用域下
●函数名称相同
●函数参数类型不同或者个数不同或者顺序不同
注意:函数的返回值不可以作为函数重载的条件
当引用做为重载条件时:
如int &a和const int &a。这是可以作为重载的,使用时如果传入变量会调用第一个,使用常量会调用第二个。
函数重载碰到默认参数时:
相当于出现了二义性,因此一定要避免这种情况。
static
static表示静态(将内容存入静态内存区域),可以修饰变量和函数
当static修饰函数内部的变量时,可以延长其生命周期到整个程序运行周期。
简单来说就是只需要初始化一次,就给本类共享
进阶
内存分区模型
C++程序在执行时,将内存大方向划分为4个区域。
程序运行前的两个区域:
代码区:
存放函数体的二进制代码,由操作系统进行管理的。
存放CPU执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
存放全局变量和静态变量以及常量
全局区还包含了常量区,字符串常量和其他常量(const修饰的)也存放在此.
该区域的数据在程序结束后由操作系统释放.
程序运行后的两个区域:
栈区:
由编译器自动分配释放,存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
由程序员分配和释放,若程序员不释放程序结束时由操作系统回收
在C++中主要利用new在堆区开辟内存
new
作用:C++中利用new操作符在堆区开辟数据。堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
语法:new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针。
1 | int* p = new int(10); //表示创建一个变量并为其赋值10,并返回该变量的指针 |
引用
概述:对已存在的变量的别名,对引用的各种操作等同于操作被引用变量本身。
语法:数据类型& 引用名 = 被引用变量;
1 | int &b = a; //表示给a起了别名b,对b操作也作用在a身上 |
本质:引用的本质在c++内部实现是一个指针常量(即指针指向不可修改,指针指向的值可以修改).
注意事项:
1.引用对象创建后,不可更改(更改指向到其它内存区域)
2.因不可更改,所以引用必须初始化
3.因必须初始化,所以引用不可为空(不可修改)
4.正常的引用需要引一块合法的内存空间,也就是右边的值是一块合法的内存空间。
场景:用于函数形参的声明(引用传参)和返回值使用
引用作函数的返回值
作用:相当于函数赋值的形式给他起了别名
注意:不要返回局部变量引用(局部变量会做一次保留,但也不能返回局部)
用法:以引用会返回的函数的调用可以作为等号左边的值
1 | int& test(){ |
常量引用
作用:常量引用主要用来修饰形参,防止误操作
场景:在函数形参列表中,可以加const修饰形参,防止形参改变实参。
test(const int& a){}
类
1 | 语法: |
访问权限
public:公共权限,类内外都可以访问
protected:保护权限,类内可以访问,类外不能,子类可以访问父类的保护内容
private:私有权限,类内可以访问,类外不能,子类不可以访问父类私有的
一般成员属性会做私有化设置,并给出get和set方法
struct和class区别
在C++中struct和class唯一的区别就在于默认的访问权限不同
区别:
●struct 默认权限为公共
●class 默认权限为私有
对象
对象的初始化和清理
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
1.析构函数,没有返回值也不写void
2.函数名称与类名相同,在名称前加上符号~
3.析构函数不可以有参数,因此不可以发生重载
4.程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
注意:构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构
构造函数
两种分类方式:
按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造(即把自身类型的对象作为参数传入,往往是const修饰)
三种调用方式:
括号法
1 | 即创建对象时直接通过括号进行选择构造函数的调用。创建对象时,调用默认构造函数时,尽量省掉(),因为会与函数的声明冲突。 |
显示法
1 | Person p1; |
隐式转换法
1 | Person p1 = 10; //相当于Person p2 = Person(10); |
拷贝构造函数的使用时机
拷贝构造函数的对象参数往往用const修饰,const Person& p
●使用一个已经创建完毕的对象来初始化一个新对象
●值传递的方式给函数参数传值(即拷贝一份不对原有进行修改)
●以值方式返回局部对象(即拷贝一份不对原有进行修改)
构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
●如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
●如果用户定义拷贝构造函数,C++不会再提供其他构造函数
深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作。
一般编译器提供的拷贝构造函数是浅拷贝。
浅拷贝很容易造成堆区内存重复释放。
深拷贝:在堆区重新申请空间,进行拷贝操作
自己实现一个拷贝函数解决浅拷贝的问题
要重新开辟一个堆空间存储拷贝的东西。
初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数(): 属性1(值1),属性2(值2) … {}
前言:以前的初始化都是利用构造函数形参传入,进行赋值。
1 | Person(int a,int b):m_A(a),m_B(b){ //也可以直接赋具体值 |
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
1 | 如 |
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
静态成员变量
特点1:所有对象共享同一份数据
特点2:在编译阶段分配内存
特点3:类内声明,类外初始化(一定要注意)
访问方式:共享之后不属于某个对象,可以通过对象进行访问,也可以通过类名进行访问(如类名::属性名)
访问权限同样适用
静态成员函数
特点1:所有对象共享同一个函数
特点2:静态成员函数只能访问静态成员变量(因为无法区分到底是哪个对象的)
访问方式:可以通过对象进行访问,也可以通过类名进行访问(如类名::方法名)
访问权限同样适用
对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置。
this指针
在C+ +中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
C+ +通过提供特殊的对象指针, this指针,解决上述问题。
概述:this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针的用途:
当形参和成员变量同名时,可用this指针来区分(如this->age = age;)
在类的非静态成员函数中返回对象本身(便于链式编程),可使用return *this,同时函数类型要用引用返回(如Person&,以引用返回是为了确保能对原本的一直修改,而不是对浅拷贝出来的修改)
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性。(因为this在此时是空肯定会报错)
const修饰成员函数
常函数:
●成员函数后加const后我们称为这个函数为常函数
●常函数内不可以修改成员属性
●成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
●声明对象前加const称该对象为常对象
●常对象只能调用常函数
●常对象的属性也不能修改,除非加关键字mutable
1 | void showPerson() const{ |
友元
作用:就是让一个函数或者类访问另一个类中私有成员
友元的关键字为friend,在类内实现语句
友元的三种实现:
●全局函数做友元
●类做友元
●成员函数做友元
1 | 全局函数做友元 |
运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
语法:自定义一个名为(operator运算符) 的函数,定义你要进行的操作,
加号运算符重载
作用:实现两个自定义数据类型相加的运算
前景:默认情况下想要实现两个自定义类型的某些属性相加,那我们需要自定义一个函数去实现。现在可以直接使用运算符重载实现。
方法一:通过成员函数重载
1 | //即在类内定义名称为operator+函数 |
方法二:通过全局函数重载
1 | //即在类外全局区域定义名称为operator+函数 |
注意:
运算符重载进行operator定义时,也可以函数重载去应对不同情况
对于内置的数据类型的表达式的的运算符是不可能改变的
不要滥用运算符重载
左移运算符重载
作用:重载左移运算符配合友元可以实现输出自定义数据类型
1 | 不能使用成员函数重载,因为无法达到cout << 对象; 的效果 |
递增运算符重载
作用:通过重载递增运算符,实现自己的整型数据
1 | 对前置的++进行重载 |
赋值运算符重载
C++编译器至少给一个类添加4个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
4.赋值运算符operator=,对属性进行值拷贝(浅拷贝)
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
1 | //利用成员函数重载 |
关系运算符重载
1 | 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作 |
函数调用运算符重载
函数调用运算符()也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数
仿函数没有固定写法,非常灵活
1 | 利用成员函数重载 |
继承
语法:class 子类 : 继承方式 父类
继承方式:公共继承、保护继承、私有继承
简单来说就是:
父类中所有非静态成员属性都会被子类继承下去,3种继承方式都只能继承下来,但是访问不到的。
若为公共继承,则父类是什么权限的属性,子类同样也是。
若为保护继承,则继承下来的父类属性都变为了保护权限。
若为私有继承,则继承下来的父类属性都变为了私有权限。
继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
总结:继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继承中同名成员处理方式
访问子类同名成员,直接访问即可
访问父类同名成员,需要加作用域(在属性前加 父类::)
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数,要想访问到父类需要添加作用域
多继承语法
C++允许一个类继承多个类
语法:class 子类 : 继承方式 父类1,继承方式 父类2…
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
菱形继承
菱形继承概念:
两个子类继承同一个父类
又有某个类同时继承者两个子类
这种继承被称为菱形继承,或者钻石继承
问题:这种继承会造成属性重复继承,造成资源浪费
解决:利用虚继承,在继承方式前加virtual。本质上是用了虚指针,找到了唯一变量
多态
多态分为两类
●静态多态函数重载和运算符重载属于静态多态,复用函数名
●动态多态:派生类和虚函数(即在函数定义前加virtual)实现运行时多态
静态多态和动态多态区别:
●静态多态的函数地址早绑定 - 即编译阶段确定函数地址
●动态多态的函数地址晚绑定 - 即运行阶段确定函数地址
1 | 在父类的函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。 |
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数语法: virtual 返回值类型 函数名(参数列表) = 0 ;
只要类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
●无法实例化对象
●子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时(也就是父类的析构函数调用时)无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
●可以解决父类指针释放子类对象
●都需要有具体的函数实现
虚析构和纯虚析构区别:
●如果是纯虚析构,该类属于抽象类,无法实例化对象
1 | 虚析构语法: |
文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件< fstream >
文件类型分为两种:
1.文本文件:文件以文本的ASCII码形式存储在计算机中
2.二进制文件:文件以文本的二_进制形式存储在计算机中,用户-般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
文本文件
写文件
1 | 写文件步骤如下: |
1 | 文件打开方式 |
读文件
1 | 读文件步骤如下: |
二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为ios::binary
写文件
1 | 二进制方式写文件主要利用流对象调用成员函数write |
读文件
1 | 二进制方式读文件主要利用流对象调用成员函数read |
模板
前言:c++中另一种编程思想叫泛型编程,主要就是利用模板技术
模板机制:函数模板和类模板
模板的特点
●模板不可以直接使用,它只是一个框架
●模板的通用并不是万能的
函数模板
1 | 函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。 |
注意事项:
●自动类型推导,必须推导出一致的数据类型T,才可以使用
●模板必须要确定出T的数据类型,才可以使用
普通函数与函数模板区别
●普通函数调用时可以发生自动类型转换(隐式类型转换)
●函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
●如果利用显示指定类型的方式,可以发生隐式类型转换
普通函数与函数模板的调用规则如下
1.如果函数模板和普通函数都可以实现,优先调用普通函数
2.可以通过空模板参数列表来强制调用函数模板,如函数<>(a,b)
3.函数模板也可以发生重载
4.如果函数模板可以产生更好的匹配,优先调用函数模板
模板的局限性
前景:有时特定的数据类型并不能直接通过泛型表示,需要加以修改
方法一:对模板函数内的运算符进行重载(太麻烦)
方法二:利用具体化Person的版本实现代码,具体化优先调用
1 | template<> bool myCompare (Person &p1, Person &p2){ |
类模板
作用:建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表。
1 | 语法: |
类模板与函数模板区别
1.类模板没有自动类型推导的使用方式,类模板只能显示指定类型,即加上<>表明类型
2.类模板在模板参数列表中可以有默认参数,如template <class T = int>
类模板中成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的:
●普通类中的成员函数一开始就可以创建
●类模板中的成员函数在调用时才创建
类模板对象做函数参数
类模板实例化出的对象,向函数传参的方式
1.指定传入的类型:直接显示对象的数据类型(用的最多)
1 | 对象初始化:Person<string,int>p("sun",10); |
2.参数模板化:将对象中的参数变为模板进行传递
3.整个类模板化:将这个对象类型,模板化进行传递
类模板与继承
当类模板碰到继承时,需要注意一下几点:
●当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
1 | class Son : public Base<int>{}; |
●如果不指定,编译器无法给子类分配内存
●如果想灵活指定出父类中T的类型,子类也需变为类模板
类模板成员函数类外实现
前景:一般情况下类内都只是函数声明,实现是在类外
1 | //构造函数类外实现 |
类模板分文件编写
1 | 问题:类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到 |
类模板和友元
1 | 全局函数类内实现:直接在类内声明友元即可 |
STL
概念
STL(Standard Template Library,标准模板库)
STL从广义上分为:容器(container),算法(algorithm),迭代器(iterator)
容器和算法之间通过迭代器进行无缝连接
STL几乎所有的代码都采用了模板类或者模板函数
六大组件
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
1 | STL容器就是将运用最广泛的一些数据结构实现出来 |
算法:常用的各种算法,如sort、find、copy、for_each等
1 | 有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms) |
迭代器:扮演了容器与算法之间的胶合剂
1 | 提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。 |
仿函数:行为类似函数,可作为算法的某种策略
适配器:一种用来修饰容器或者仿函数或迭代器结构的东西。
空间配置器:负责空间的配置与管理。
常用容器
vector
概述:向量,以顺序存储的线性表。vector为可变长数组动态数组,定义的vector数组可以随时添加数值和删除元素。
头文件:#include<vector>
一维初始化:
1 | vector<数据类型> a; //定义了一个名为a的一维数组,数组可以存储int/double/结构体等类型数据 |
二维初始化:
1 | //初始化二维均可变长数组 |
添加元素到尾部:vector.push_back(值);
获取元素数量:即长度,vector.size();
删除最后一个数据:vector.pop_back()
是否为空,为空返回真,反之返回假:vector.empty()
返回首元素的地址:begin()
返回最后一个元素的后一个位置的地址:end()
将数据插到索引处,原本的要往后移:insert(vector.begin() + 索引,值) //索引是从0开始
将索引处的数据删除掉:erase(vector.begin() + 索引)
排序:
1 | vector<int> a(n + 1); |
练习1
1 | 题目描述:一个数如果恰好等于它的各个因子(该数本身除外)之和,如6=3+2+1,那么称该数为“完数”;若因子之和大于该数,则称其为“盈数”。求出2到60之间的所有“完数”和“盈数”。 |
string
deque
stack
特点:先进后出,栈顶top进出
头文件:#include<stack>
定义:stack<数据类型> 栈名;
方法:
①size():返回栈元素个数
②push():加入元素到栈中
③pop():弹出栈顶元素
④top():获取栈顶元素的内容
⑤empty():判空
queue
特点:先进先出,对头出队,队尾入队
场景:公平的等待,广度优先遍历
头文件:#include<queue>
定义:queue<数据类型> 队列名;
方法:
①push:入队
②pop:出队
③empty:判断是否为空
④front:队首元素
list
set
Map
头文件:#include<map>
初始化:map<数据类型, 数据类型> mp; //数据类型如int,string,double
特性:
1 | map会按照键的顺序从小到大自动排序,键的类型必须可以比较大小 |
添加元素:
1 | mp.insert(pair<string,string>("fruit","水果")); |
遍历访问:
1 | //方式一:迭代器访问 |
方法:
1 | mp.erase(key) //根据映射的键删除键和值 |
递归
核心:如何把问题为n的规模用n-1的规模去表示
1 | 如:传统3根柱子n个盘子的汉诺塔问题 |