引言
- 王建伟老师的《C++新经典》的学习笔记
2 数据类型、运算符与表达式
2.1 常量、变量、整型、实型和字符串
项目要先 编译、链接、生成可执行程序 ,然后才能运行。
2.1.2 C语言的数据类型
其中包括:
- 基础类型:数值类型(整型、浮点型)、字符类型
- 构造类型:数组、结构体(struct)、共用体(union)、枚举类型(enum)
- 指针类型
- void空类型
其中在64位系统中,int、float类型为4字节,double、long、long long类型为8字节,一个字节为8位二进制数
2.1.3 常量和变量
变量定义:
类型名 变量名[ = 变量初始值]
使用[]包括的内容可以省略
变量必须先定义才可以使用
变量命名方法:推荐第一个字符表示类型,用几个单词的组合表示含义,每个单词的第一个字符大写
2.1.4 整型数据
- 八进制数:以0开头的数字
- 十六进制:以0x开头的数字
整型变量主要包括基本型int类型、长整型long int(简写为long)类型、无符号类型(unsigned),可以使用sizeof()函数获取变量所占字节数。
- 常量的类型的特殊写法
- 常数后加U或u,表示无符号整型类型存储,相当于unsigned int
- 常数后加L或l,表示长整型存储,相当于long
- 常数后加F或f,表示以浮点存储,相当于float
2.1.5 实型数据
实型数据是指实数,也称为浮点数,浮点数在内存中都是以指数形式存储。
如何区分float和double两种浮点数?
精度不同,float在内存中一般占4字节,而double在内存中一般占8字节,float提供7位有效数字,而double提供15-16位有效数字。
给浮点数赋值0.51,但显示为0.509999990的原因?
Ans:计算机中,会把十进制数转换为二进制数保存,在查看时在转换为十进制数显示,在这中间存在着一些出发操作,会导致在二进制转换为十进制的过程中丢失精度
2.1.6 字符型数据
推荐单引号内只能放一个字符的规定 ^3eb832
字符变量
将一个字符常量放在字符变量中,实际上是以字符对应的ASCII码存储在内存中
ACSII码,空格字符对应32、数字0对应48、A对应65、a对应97
2.1.7 字符串变量
跳转至该位置
规定,单引号包含单个字符串。
字符串变量为双引号包含一串字符
'a’与"a"的区别?
'a’表示单个字符变量,在内存中只占一个字节,而"a"表示字符串变量,在内存中占两个字节
2.1.9 数值型数据之间的混合运算
不同类型的数值型数据进行混合运算时,系统会尝试进行变量类型统一,然后再进行计算。
转换规则:
- int转为unsigned转为long转为double,当类型不同时进行转换。
- char和short转换为int,float转换为double,强制转换,在该类型进行运算时,必须转为int或double类型,再进行计算
2.2 算数运算符和表达式
2.2.1 C语言的运算符
共13个大类,包括算数运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、条件运算符、逗号运算符、指针运算符、求字节运算符、强制类型转换运算符、成员变量运算符、下标运算符、其他。
2.2.3 运算符优先级问题
单目运算符>双目运算符>三目运算符>赋值运算符
单目运算符、条件运算符、赋值运算符为从右到左结合,其他的为从左到右结合。
2.2.4 强制类型转换运算符
1 | >表达式一般形式 |
2.2.5 自增和自减运算符
1 | ++i; 先加一再使用 |
-i++ 表示-(i++);i+j表示(i) +j
3.1 C语言的语句和程序的基本结构
1 | continue 结束本次循环,开始下次循环是否执行的判断 |
3.1.2 程序的三种基本结构
- 顺序结构
- 选择结构
- 循环结构
- 当型循环:先判断再执行
- 直到型循环:先执行再判断
3.2 数据的输出与输入
- 当使用尖括号<>包括头文件时,表示在系统目录中寻找头文件
- 当使用双引号""包括头文件时,表示在源代码所在目录中搜索头文件,再到系统目录中搜索头文件
1 | printf()函数 |
百分号的输出方法:
- 使用两个百分号输出
- 使用%c输出‘%’
- 使用%s输出“%”
3.2.2 数据的输入
在程序中,最好只使用一个getchar()输入,避免结束输入时,第二个getchar()函数读取回车符号,影响程序的正常预期.
scanf(格式控制字符串,地址表列);
- 第二个参数为地址表列,使用的是地址,不要忘记 & 符号对变量取地址
- 要注意输入与格式控制字符串形式相同
4 逻辑运算和判断选择
- 优先级顺序:算术运算符>关系运算符>赋值运算符
逻辑运算符:
- && 与运算符:同真为真,有假出假
- || 或运算符:有真出真,同假为假
- ! 非运算符:反转
if()语句:
- if(判断表达式)语句;
- if(判断表达式)语句1; else 语句2;
- if(判断表达式1) 语句1; else if(判断表达式2) 语句2;······
switch()语句:
注意break语句为跳出switch()结构终止该结构的运行,注意其使用的技巧
5 循环控制
5.1 goto语句
goto语句需要搭配==语句标号:==语句一起使用,goto 语句标号;为使用方式.
主要用途:
- 与if语句一起使用构成循环
- 从循环体内跳转到循环体外(不建议使用)
- goto语句不能跨函数使用
5.4 for语句
for(表达式1(初始值);表达式2(结束条件);表达式3(变化量)) 内嵌的语句;
- 表达式1省略,循环外需要初始化变量
- 表达式2省略,需要使用break语句终止循环
- 表达式3省略,需要在循环内改变循环量
- 三个表达式都可以省略,但都需要对应的操作
- 三个表达式都省略,且不加对应的操作,for(;😉{}相当于while(1){}
- 表达式1可以设置其他变量初始化
- 表达式3执行很多次,但表达式1只执行1次
break语句和continue语句的区别?
continue只结束本次循环,而不是结束整个循环,而break是结束整个循环
6 数组
6.3 字符数组
6.3.3 字符串和字符串结束标记
C语言中规定了一个字符串结束标记,用’\0’代表,如果一个字符串的第十个字符为‘\0’,则该字符串的长度为9个,即遇到‘\0’表示字符串结束,该字符前面的字符构成字符串.可见10个可见字符的字符串,其可见字符若每个占用一个字节,则整个字符串在内存中占用11个字节
注意:设置字符串变量的时候需要考虑–字符串结束标记–,则需要可见字符串长度加一设置大小
对于双引号包含的字符串,系统会自动往末尾添加一个’\0’,若人工添加也是可以的,若没有字符串结束标记也是可以的
6.3.5 字符串处理函数
strcat(字符数组1,字符数组2);
把字符数组2连接到字符数组1的后面,存放在字符数组1中.
说明:
- 字符数组必须足够大,能够容纳连接后的新字符串.
- 连接前两个字符数组都包括字符结束标记,但连接时会自动删除字符数组1的字符结束标记
- 连接后字符数组2没有变化
strcpy(字符数组1,字符数组2);
把字符数组2复制到字符数组1中.
不能使用赋值语句对字符数组直接赋值,只能对单个字符进行赋值
strcmp(字符数组1,字符数组2);
字符串比较,若字符数组1>字符数组2,使用的是ASCII码进行比较,返回一个正整数,相等返回0.
strlen(字符数组);
返回字符串的长度,不包括字符结束标记.
7 函数
7.2 函数调用的一般形式
函数声明的一般形式:
返回类型 函数名(形参列表);
- ==函数定义和函数声明的区别?
- 函数定义里面包含函数体,函数体中的代码确定了函数需要执行的功能,而函数声明,只是对一定义的函数进行说明,不包括函数体.函数声明可以提前指明该函数的参数类型、返回值类型等,让函数的调用者明确的知道这些信息.==
- C语言不允许嵌套定义函数,但可以嵌套调用函数
- 函数的递归调用:执行递归函数将反复调用其自身,每调用一次就进入新的一层,递归函数必须有结束条件
7.2.3 递归调用的出口
利用递归调用计算阶乘例程:
1 | int dg_jiecheng(int n) |
7.3.3 递归调用的优缺点以及是否必须用递归
优点:代码简洁
缺点:
- 理解起来困难
- 若调用层次太深,调用栈可能会溢出
- 效率和性能不高
汉诺塔问题:
大梵天创造世界时造了三根金钢石柱子,其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
解决方法:
- 使用递归调用方法.(将前n-1个盘子看成一个整体,递归调用函数,不断对可调动的盘子调动)
- TODO: 使用其他方法(之后详解).
7.4 数组作为函数参数
7.4.2 数组名作为函数实参
- 将数组名作为函数实参进行传递时,传递的是数组的首地址,此时函数中的形参也应该使用数组名(或者数组指针)
- 称为地址传递,形参数组中的各个元素的值如果发生变化,等价于数组中的元素的值发生了相应的改变
- 实参为数组名,形参也应该为数组名
- 实参数组与形参数组类型必须一致
- 形参数组的大小可以不指定,即使指定也可以与实参数组大小不一致
7.4.3 用多维数组作为函数实参
形参数组在定义时,可以省略第一维的大小,但不可以省略第二维的大小.实参是多少行多少列,形参应该尽量和实参大小一致.
7.5 局部变量和全局变量
7.5.1 局部变量
- 形参也是局部变量
- 不同函数内部可以使用相同的变量名,之间互不干扰
- 复合语句(用括号{}包括的语句),其中定义的参数为局部变量,生命周期只在定义的范围内生效
7.5.2 全局变量
- 如果某个函数想引用在它后面定义的全局变量,可以使用extern做外部变量说明.在进行外部变量说明时,不可以进行赋值操作.
- 严格区分外部变量和全局变量说明:
- 全局变量定义只能有一次,位于所有函数之外,定义时即分配内存.
- 外部变量,可以在一个文件中可以声明很多次.
- 同一个源文件中,局部变量和全局变量同名,则在局部变量的作用范围内,全局变量不生效.
- 如果在一个源程序文件中定义的全局变量想在项目其他源文件中使用,则需要在其他需要使用的源文件中添加extern关键字做外部变量声明即可使用.
7.6 变量的存储和引用与内部和外部函数
程序在内存中的存储空间分为程序代码区、静态存储区、动态存储区
- 全局变量存储在静态存储区
- 函数形参、局部变量、函数调用时的现场数据和返回地址存储在动态存储区
7.6.2 局部变量的存储方式
局部变量声明为局部静态变量时,需要添加static关键字,则在函数调用结束后,该局部变量不会被释放.
- 在静态存储区中分配内存,程序在整个运行期间不会释放
- 局部静态变量在编译时赋初值,且只赋一次初值.之后调用函数时,不再进行赋初值操作,只是保留上一次运行结束的值
- 定义局部静态变量时,如果不赋初值,则==编译时,自动赋初值为0.
- 虽然局部静态变量在函数调用结束后依然存在,但在其他函数中不能引用
- 局部静态变量长期占用内存
- 如非必要,不要过多使用局部静态变量
7.6.3 全局变量跨文件使用
跳转至该位置
跨文件使用的全局变量extern用法,一般放在程序的开头部分
若需要全局变量,只能在本文件中使用,则需要在该全局变量声明时,添加==static
==关键字声明为静态全局变量
7.6.4 函数跨文件使用
内部函数定义:只能在本文件中使用(需要添加static关键字)
static 返回类型 函数名 (形参列表){…}
7.6.5 static关键字用法总结
8 编译预处理
C语言提供的三种预处理操作:
- 宏定义
- 文件包含
- 条件编译
8.1 宏定义
- 宏名一般使用大写字母表示
- #define的有效范围到程序文件结束的部分,不可跨文件使用
- 可以使用\undef结束宏定义的作用域
8.1.2 带参数的宏定义
一般形式:
#define 宏名(参数表) 被替换的内容
作用:用右边的“被替换的内容”代替“宏名(参数表)”
-
对一般形式中提到的“被替换的内容”,要从左到右处理
-
如果“被替换的内容”中有“宏名”后列出的形参,则程序代码中相应的实参代替形参.
-
如果“被替换的内容”的项并不是“宏名”后列出的形参,则保留.
-
一般在形参外面加一个括号
-
宏定义时,宏名和带参数的括号之间不能加空格
-
函数调用是先求实参表达式的值,然后传递给形参;带参数的宏只进行简单的替换,展开时不求值
-
宏展开只占用编译时间,不占用运行时间;函数调用占用运行时间,不占用编译时间.
8.2 文件包含和条件编译
8.2.1 文件包含
常把一些宏定义,函数说明,甚至一些公共的#include命令、外部变量说明(extern)等,都可以写到一个.h文件中,然后在源程序中#include这个头文件即可
#include头文件允许文件嵌套使用
8.2.2 条件编译
#ifdef 标示符
程序段1
#else
程序段2
#endif
或
#ifndef 标示符
程序段1
#else
程序段2
#endif
或
#if 表达式
程序段1
#else
程序段2
#endif
或
#if 表达式1
程序段1
#elif 表达式2
程序段2
#else
程序段3
#endif
9 指针
在常见的X86、X64平台下,int类型占4字节,char占1字节,float占4字节,double占8字节
在x86平台下,整型指针变量占用4字节地址,而x64平台下,整型指针变量占用8字节地址
- 指针变量:用于存储另一个变量的地址
- 指针变量的值:为另一个变量的地址
- 指针就是一个地址,指针变量就是存储其他变量地址的量
9.2 变量的指针和指向变量的指针变量
定义指针变量:
类型标识符 * 标识符;
- 在定义指针变量时,指针变量前面具有*,表示正在定义一个指针变量,但在使用该指针变量时,指针前面的*是没有的
- 一个指针变量只能指向同一个类型的变量
引用指针变量:
- & 取地址运算符
- * 指针运算符(间接访问运算符)
==*p1++ 从右到左结合,等价于*(p1++)
- 要注意将指针作为函数参数传入函数后,操作指针可能会导致指针指向发生变化,但实际指向的变量并没有发生变化(例教材P139)
- 可以间接在函数中改变指针变量所指向的变量的值,从而实现在调用函数内改变外部变量的值
9.3 数组的指针和指向数组的指针变量
数组名等于数组首地址(数组第一个元素的地址),所以数组名a等效于&a[0]
指向数组的指针变量,可以带下标,如p[i]与*(p +i)等效,等效于数组a的a[i]变量
*(p++)与*(++p)的区别?
++在变量p的后面,表示先用再加,在变量p的前面表示先加再用;
若p指向a[0],则*(p)表示先用后加,则使用时,p指向a[0],当使用结束后p指向a[1]
9.3.5 指向多维数组的指针和指针变量探究
二维数组:
- a 为二维数组名,数组首地址,第0行首地址
- a + 1,表示第1行首地址
- a[0],&a[0][0],a,(a+0),&a[0] 表示第0行首地址(第0行第0列元素地址)
- a[0]+1,&a[0][1],*a+1 表示第0行第1列元素地址
- (a[0] +1),a[0][1],(*a +1) 表示第0行第1列元素值
9.3.6 指针数组和数组指针
- 指针数组
int * p[10]
表示数组,但是数组的元素都是指针. - 数组指针
int *p;
数组指针是指向数组地址的指针,其本质为指针;
指针变量,名称为p,表示这个指针变量用来指向含有10个元素的一维数组
指向一维数组的指针变量
设一维数组为a[n],
定义方法:*指针变量名 (即*P)
指向二维数组的指针变量
设二维数组为a[m][n],
定义方法:*指针变量名 (即(*P)[n])
“长度”表示二维数组分解为多个一维数组时,一维数组的长度,也就是二维数组的列数。
1 | //数组指针: |
9.3.7 多维数组的指针作为函数参数
一维数组的地址可以作为函数参数传递,多维数组的地址也可以作为函数参数传递
C语言中,对于字符串常量会在内存中开辟一块专门的地址用来存放字符串常量,这段内存是只读的,不可以进行修改
9.5 函数指针和返回指针值的函数
指针变量指向函数,从而可以通过指针来调用所指向的函数
定义:
数据类型标识符 ( * 指针变量名) (形参列表);
指针变量名 = 函数名; //进行赋值,将函数的地址赋值给指针
- int *p(int x, int y); 表示这个函数的返回值是指向整型变量的指针,表示定义返回整型指针的函数
- int (* p)(int x, int y); 表示p指针指向函数,表示函数指针
9.5.2 把指向函数的指针变量作为函数参数
指向函数的指针变量可以作为参数,从而实现函数地址的传递,也就是将函数地址传递给形参.
用函数指针变量作为形参和实参可以方便的实现调用函数的切换,增强程序的灵活性和扩展性.
9.5.3 返回指针值的函数
一般定义形式:
数据类型 * 函数名(参数列表){…}
在构造子函数时,需要注意局部变量的生命周期,避免因为局部变量被系统回收而造成程序崩溃(特别需要注意返回变量的引用)
9.6 指针数组、指针的指针与main函数参数
指针数组定义:
跳转至该位置
类型标识符 * 数组名[长度]
9.6.3 指针数组作为main函数参数
main函数需要改造一下才可以接受系统传递进来的参数:
int main(int argc, char * argv[])
main函数接受两个参数:
- 第一个参数是整型数据
- 第二个参数是指针数组(作为函数形参)
10.1 结构体变量定义、引用与初始化
10.1.1 结构体类型定义
struct 结构体名
{
成员列表
}
10.1.2 结构变量的定义方法
- 形式1:定义完结构类型后定义变量
struct 结构体名 变量列表;
- 形式2:定义结构类型时定义变量
struct 结构体名
{
成员列表
}变量名列表;
- 形式3:直接定义结构变量
struct
{
成员列表
}变量名列表;
说明:
- 使用结构体,一般需要定义一个结构体类型,在定义结构变量
- 结构体内可以套结构
- 结构体内成员名可以和程序中的变量名相同
10.1.3 结构体类型变量的引用
1 | // 引用方式: |
10.2 结构体数组和结构体指针
10.2.2 结构体指针
定义:
struct 结构体类型名 * 指针名;
赋值:
只能指向一个结构体类型的变量,或者结构体类型数组中的某一个元素,不可以指向其中的具体成员
访问结构体变量成员
- 使用结构体成员运算符“.”
- 使用结构体成员运算符"->"
如:
struct students stu;
struct students *p;
p = &stu;
(*ps).num = 1; (*的优先级比“.”低)
ps -> num =1;
- 可以使用指向结构体的指针作为函数参数
10.3 共用体、枚举类型与typedef
10.3.1 共用体
共用体和结构体的区别?
共用体的成员会占用同一段内存,而结构体中的成员会占用不同的内存
定义:
union 共用体名
{
成员列表
}变量列表;
共用体类型的定义和共用体变量的定义与结构体的定义和结构体变量的定义相似
共用体成员共占一段内存,所以占用内存的大小等于所占用内存最大的成员的大小
每个瞬间只有一个成员起作用,其他成员不起作用
10.3.2 枚举类型
定义枚举类型:
enum 枚举类型名
{
值
}变量列表;
- C语言会自动按照定义时的顺序规定它们的值,并且值是从0开始的.
- 可以手动在定义枚举类型的时候设置默认的枚举常量的值,后面的枚举常量的值为前一个的值+1
- 不可以将整数直接赋值给枚举变量,但使用强制类型转换是可以的(enum 枚举类型名);
10.3.3 用typedef定义类型
typedef是用于定义类型名的
typedef 原类型名 新类型名;
习惯上,用typedef定义的类型名用大写字母表示
11 位运算
- 1个字节是由8个二进制位组成的
- & 按位与
- | 按位或
- ^ 按位异或:两个位相同,返回0,不同为1
- ~ 取反
- << 左移
- >> 右移
可以使用该代码定义位操作
#define BIT(X) (1<<(X)) 带参数的宏定义
12 文件操作
文本文件中,每个字节存放一个ASCII码,代表一个字符
大端存储与小端存储?
-
大端存储:低字节存储在高地址,高字节存储在低地址
-
小端存储:低字节存储在低地址,高字节存储在高地址
-
如果在文本文件中,以16进制保存数字,保存比较节省空间,但需要人工阅读
12.2 文件的打开、关闭、读写
文件在进行读写操作之前,必须先打开,在读写结束后,必须关闭文件
文件打开:
FILE * fp; //FILE为一个结构体,fp是指向结构体的指针(文件指针)
fp = fopen(文件名,文件使用方式);
文件使用方式:
-
r 只读
-
w 只写(文件存在,则清空重写,否则创建文件)
-
a 追加(末尾追加)
-
rb 只读(打开二进制文件)
-
wb 只写(二进制文件)
-
ab 追加(二进制文件)
-
r+ 读写(打开文本文件)
-
w+ 读写(创建文本文件)
-
文件打开后,会返回一个位置指针(*char),代表当前从文件的哪个位置开始读写.
-
文件不存在时fopen函数会返回空(NULL)
文件关闭:
fclose(文件指针);
文件关闭时,会将缓冲区的数据立即写到磁盘上
文件读写:
- fputc(ch,fp); //将ch写入磁盘文件中
- fgetc(fp); //从磁盘文件中读取一个字符
- feof(fp); //判断文件是否结束(结束返回1,否则返回0)
12.3 将结构体写入二进制文件再读出
fwrite(butter, size, count, fp);
向文件写入数据
butter:指针或者地址(将要写入的数据)
size:写入文件的字节数
count:要写入多少size的数据项
fp:文件指针
返回值,如果fwrite失败,返回0,否则返回count值
- 对于要写入的结构体,结构体成员中不要出现指针型成员(因为指针类型成员地址可能失效)
- 可以使用==#pragma pack(1)设置代码对齐方式为1字节对齐,即不要对齐==,可以避免程序在Linux平台读数据出错
fread(buffer,size,count,fp);
向文件读出数据
butter:指针或者地址(将要写入的数据)
size:写入文件的字节数
count:要写入多少size的数据项
fp:文件指针
返回值,如果fwrite失败,返回0,否则返回count值
13 C++基本语言
13.1 语言特性、工程构成与可移植性
C语言是过程式的语言
C++为面向对象的语言,其与面向过程语言的最大区别是:在基于面向对象的程序设计中,额外应用了继承性和多态技术
面向对象的程序设计的优点:
- 易维护
- 易扩展
- 模块化
C++程序的头文件一般以.h居多,此外还有.hpp;
hpp文件一般来说就是把定义和实现都包含在一个文件中,可以有效的减少编译次数
C++为编译型语言
在程序执行前,需要一个专门的编译过程,将程序编译成二进制文件,执行的时候不需要重新编译,直接使用编译结果.
13.2 命名空间简介与基本输入输出精解
命名空间就是为了防止名字冲突而引入的一种机制.
定义:
namespace 命名空间名
{
其他函数、
}
访问:使用“::”作用域运算符
命名空间名::实体名
- 命名空间定义可以不连续,可以写在不同的位置,甚至可以写在不同的源文件中
- 注意不同命名空间中相同的函数在同一个函数中调用,若冲突则:
- 要么不声明两个命名空间
- 要么不同命名空间中的函数不要同名
- 要么调用命名空间中的函数时添加“ 命名空间:: ”表达方式
13.2.2 基本输入输出
C++中输入输出的标准库是iostream库(输入/输出流)
- std是标准库中的一个命名空间
- std::cout 是一个对象,为标准输出
- std::cin为基本输入
- << 向左扎,为输出运算符,与std::cout结合表示将内容扎到cout中,表示将<<右侧的内容写到cout屏幕中
- std::endl 相当于函数指针.其作用为: 1. 输出换行符 2.刷新输出缓冲区,调用flush函数强制输出缓冲区中所有数据,然后把缓冲区中的数据清除
为什么要有输出缓冲区?
将数据临时保存到输出缓冲区,当缓冲区满了、或者程序执行到main函数的return正常结束时、或者使用std::endl函数(后面会调用flush()函数)等情况,程序一次性将数据写入磁盘,提升速递,降低内存消耗
注意<<符号为右结合性
13.3 auto、头文件防卫、引用与常量
13.3.1 局部变量和初始化
C++中,变量随时定义即可,不必像C语言中需要在函数开头定义
在C++新标准中,可以使用“{}“对变量进行初始化.
- 例如: int abc{5};
==在C++中的新标准的好处:
int abc = 3.5f; //编译成功,自动进行截断操作
int abc{3.5f}; //编译错误,语法报错
这样做数据不会被截断==
同样也可以使用“()”对变量进行初始化操作
13.3.2 auto关键字简介
auto在C++11中的全新含义:变量的自动推断
变量的自动推断发生在编译期,所以在使用时不会造成程序的效率降低
13.3.3 头文件防卫式声明
为了避免重复定义头文件,在声明头文件时,需要添加头文件防卫式声明:
#ifndef 文件名
#define 文件名
代码程序
#endif
在每一个.h文件的#ifdef后面的定义的名字都不一样,不可以重名
13.3.4 引用
引用是为变量起的另一个名字,一般使用" & "符号, 引用和原变量所占的内存是同一段内存(对初学者来说)
定义引用变量时必须进行初始化操作
引用数据类型 & 变量名 = 被引用变量名称; //==(引用变量必须绑定到变量或者对象上,不能绑定到常量上)
- & 在=左侧表示引用
- & 在=右侧表示取地址
13.3.5 常量
- const常量
定义变量时,在前面添加const关键字,即为常量 - constexpr常量
在C++11中的新关键字,代表一个常量,表示在编译时进行求值,可以提升运行时的性能
- 在书写constexpr返回类型的函数时,需要格外注意:如
-
- 在定义变量时,不可定义未初始化的变量(定义变量必须进行初始化操作)
- 2.传入constexpr函数的形参对应的实参必须为常量或常量表达式
-
- 其中代码需要尽量简单
-
- 加了constexpr修饰的函数不但能用在常量表达式中,也能用在常规的函数调用中
常量表达式不可以强制转换得到
13.4 范围for、new内存动态分配与nullptr
13.4.1 范围for语句
范例:
int v[]{12,13,14,15,16};
for{auto x:v} //此为范围for语句
{
代码;
}
此例多了一个复制的动作,可以使用引用避免复制,提高运行效率
for { auto &x : v}
一般来说,一个容器只要内部支持begin和end成员函数用于返回一个迭代器,能够指向容器的第一个元素和末尾元素的后面,这种容器就饿可以支持范围for语句
13.4.2 动态内存分配问题
C++中内存被分为5块区域:
- 栈:局部变量创建时存储的位置
- 堆:使用malloc/new申请,free/delete释放的区域
- 全局/静态存储区
- 常量存储区
- 程序代码区
- 内存分配和释放必须成对出现
- free和delete不可以重复调用
- new相较于malloc,new还进行一些额外的初始化工作;delete相较于free,delete还会进行一些额外的清理工作
堆和栈的区别:
栈空间有限,使用便捷;堆空间自由决定分配的内存大小。
- malloc和free
在C语言中成对出现,用于在堆中分配内存
一般形式为:
void * malloc(int NumBytes);
void free(void * Ptr)
malloc为向系统申请NumBytes字节的内存空间,分配成功返回被分配内存的指针,否则返回NULL空指针
free为回收指针Ptr对应的内存
strcpy_s:为strcpy的安全版本,能够检测所容纳的元素是否越界。
2. new和delete
new和delete为运算符,不是和malloc和free一样的函数
在C++中从堆中分配内存
new的使用方法:
指针变量名 = new 类型标识符;
指针变量名 = new 类型标识符(初始值);
指针变量名 = new 类型标识符[内存单元个数];
13.4.3 nullptr
nullptr为C++11中引入的新关键字,表示“空指针”
typeid()函数用于取类型,其中name成员可打印出类型名
==使用nullptr可以避免在整数和指针之间发生混淆
- NULL 其类型为int类型,其实就是0
- nullptr表示指针类型
- NULL和nullptr会导致调用不同的重载函数
- 在实际中,对于指针的初始化,能用nullptr尽量使用nullptr
13.5 结构、权限修饰符与类简介
当形参类型使用引用时,在调用函数内所改变的值在主调函数中也会发生相应的改变。实参和形参代表的是同一段内存地址,在传递参数时不存在参数值的复制。
在C++中,结构不仅可以定义成员变量,还可以定义成员函数(方法),可以使用对象名.成员函数名(实参列表)进行调用。
13.5.2 public和private权限修饰符
- public为“公共”的,可以被外部访问,类似类的外部接口
- private为“私有”的,表示只能被该结构或者类内部定义的成员函数使用
- 成员函数可以直接访问成员变量(不管成员变量是否为private都可以访问)
默认情况下,struct定义的结构,其内部的成员变量和成员函数都是public
1 | struct 结构类型名 |
在C++中引入类的概念,使用class定义
结构使用struct定义(与C语言一致)
类的某个变量成为对象,为类的实例化,类中间的成员函数称为方法
- 类的内部变量默认为private
- C++中结构的继承默认为public,而类的继承默认为private
13.6 函数新特性、inline内联函数与const详解
13.6.1 函数回顾与后置返回类型
- 在函数声明中,可以没有形参名
- 在函数定义中,需要有形参类型和形参名,但若该形参不需要使用,可以不添加函数名,但是在调用是必须给出该位置的实参
后置返回类型:
在函数声明或定义中,把返回类型写在参数列表之后
1 | auto 函数名(形参列表) -> 函数返回类型; //函数声明 |
13.6.2 inline内联函数
为避免函数调用消耗系统资源,解决函数体很小,调用频繁的函数消耗系统性能,使用inline关键字
- 系统尝试将调用该函数的动作替换为函数的本体
- 内联函数的定义写在头文件中
- inline函数应该尽量简单,避免循环、分支、递归调用等
- 可以将constexpr函数看为更加严格的内联函数(constexpr自带inline属性)
函数特殊写法:
- 函数只可以被定义一次,但是可以声明很多次
- 在C++中,更加习惯使用引用类型的形参来代替使用指针类型的形参
- 函数重载时,const关键字会被同名函数忽略
13.6.4 区别
const char * 、char const * 与char * const三者的区别
- const char *p
定义一个常量指针p(p指向的内容不可以通过p来修改) - char const *p
等价于const char *p - char * const p
定义一个指针常量p(p不可以指向其他内容,但可以修改指向的目标中的内容)
13.6.5 函数形参中带const
如果不希望在函数中修改形参的值,建议形参使用常量引用
- 可以防止无意中修改了形参的值导致实参值被改变
- 实参类型可以更加灵活
使用常量引用,不仅仅可以接收普通引用作为实参,还可以接收常量引用作为实参
13.7 string类型
string位于std命名空间中,位于string头文件中
(string为一个对象)
定义和初始化:
1 | string s1; |
常用操作:
- .empty()方法 :返回布尔量判断是否为空
- .size或.length方法 :返回字节数量
- s[n] :返回s中的第n个字符
- s1+s2 :字符串连接
- s1=s2 :字符串对象赋值
- s1==s2 :判断两个字符串是否相等
- s1 !=s2 :判断两个字符串是否不相等
- s.c_str() :返回一个字符串s中的内容指针(==把string对象转换为C中的字符串样式)
- 范围for语句 :for(auto& x:s1)
13.8 vector类型
代表一个容器、集合、或者动态数组,可以将一堆相同类型的类对象放到vector容器中
vector位于std命名空间中,vector头文件中
定义:
1 | vector <类型> 对象名; |
vector容器不能用来装引用(引用只是一个别名,并不是一个对象)
一般使用()表示与元素数量相关的操作,用{}表示只与元素内容相关的内容
13.8.3 vector对象上的操作
- .empty :判断是否为空
- .push_back() :向vector末尾添加元素
- .size :返回元素数量
- .clear :移除所有元素,容器清空
- v[n]
- =,==,!=,
- 范围for语句,在范围for语句中,不要改变vector的容量,增加、删除元素都不可以
13.9 迭代器精彩演绎、失效分析及弥补、实战
在C中很少通过下标访问string或vector中的元素,通常使用迭代器访问
通过迭代器,可以读取容器中的元素值,修改容器中某个迭代器代表的元素的值,可以像指针一样使用,–等运算符进行元素访问
C++中每种容器都定义了迭代器类型,以vector容器举例:
定义容器:
1 | vector <int> iv = {100,200,300}; |
13.9.3 迭代器begin,end、反向迭代器rbegin,rend操作
- 迭代器
- 反向迭代器
- rbegin:指向最后一个元素
- rend:指向第一个元素前面的位置
1 | // begin成员函数:返回一个迭代器类型 |
13.9.4 迭代器运算符
- *iter //返回迭代器元素的引用
- ++iter,–iter
- ==,!=
- 结构成员变量的引用
13.9.5 const_iterator迭代器
类似一个常量指针,若容器对象为常量对象,则必须使用const_iterator迭代器
C++11中引入新的关键字,cbegin与cend返回常量迭代器(不管容器是否为常量容器)
13.9.6 迭代器失效
如果在一个使用了迭代器的循环中插入元素到容器,当插入元素后应立即跳出循环体,不应该继续使用带迭代器操作容器
容器清空:
- 使用clear方法清除
- 使用迭代器一个一个清除
1 | while(!iv.empty()){ |
13.10 类型转化:static_cast、reinterpret_cast等
C风格的强制类型转化:
- (类型)待转换的内容;
- 类型 (待转换的内容);
C++中强制类型转换的通用形式:
强制类型转换名 <type>(express);
- static_cast :静态转换
- 与C语言强制类型转换相似
- 派生类转换为基类
- 不可用于指针类型之间的转换
- dynamic_cast :在转换时会进行类型识别和检查
- 主要用于父类转换为子类
- const_cast: 去除指针或者引用的const属性
- 注意只能用于原先不是常量,后来转换为常量的变量的去除,(原先是常量的去除为为定义行为)
- const_cast不可以改变表达式的类型
- reinterpret_cast :用来处理无关类型之间的转换
- 可以将整型转换为指针、一个类型指针转换为另一个类型的指针
- 可以将一个指针转换为整型
static_cast、reinterpret_cast可以替代C语言风格的类型转换
14 类
14.1 成员函数、对象复制与私有成员
设计类思考的角度:
- 站在设计和实现者的角度考虑,类的数据存储布局、必要的成员变量和成员函数
- 站在使用者的角度,可以访问的接口、对外开放仅供内部函数使用的接口
- 设计父类供子类继承时,如何设计父类
类的成员主要包括:成员变量、成员函数等
访问类成员:
- 使用“对象名.成员名”访问成员
- 使用指向这个对象的指针访问: 指针名->成员名
注意点:
- public修饰的成员可供外部访问,而private修饰的成员不可以被外部访问
- class成员默认为private,而struct默认为public的
- 建议:没有成员函数只有成员变量的数据结构使用struct;而有成员函数的数据结构一般使用class
14.1.3 成员函数
类可以被定义多次,而全局变量只能被定义一次
在C++中,若类定义和类实现放在不同文件时,其成员函数需要使用==“类名::成员函数名”==表明该成员函数为该类的成员函数(::为作用域运算符)
如:
1 | void Time::initTime(int tmphour, int tmpmin, int tmpsec) |
14.1.4 对象复制
可以使用==”=“、”()“、”{}“、”={}“==符号实现对象的复制操作
14.2 构造函数详解、explicit与初始化列表
- 将成员函数的声明和实现都写在类的内部,称为成员函数的定义
- 将成员函数的声明写在类的内部,外部的成员函数代码称为:“成员函数的实现”
构造函数:与类名相同,系统会自动调用,目的就是为了初始化类对象的数据成员(成员变量)
- 构造函数无返回值
- 构造函数不可以手工调用,会发生程序错误,系统会自动调用
- 正常情况下,构造函数声明为public
- 构造函数中含有的参数,在创建对象时需要给定相应的参数
- 一个类可以存在多个构造函数,必须为该类对象的创建提供多种方法,多个构造函数需要在参数数量或者参数类型上有所不同
对象的复制会调用拷贝构造函数
14.2.4 函数默认参数
任何函数都可以有默认参数,对于传统函数,默认参数一般放在函数声明中,而不放在函数实现中,除非函数只有定义
- 对于类中的成员函数,默认参数写在类的成员函数声明中,一般写在.h文件中
- 具有多个参数的函数中指定默认参数,默认参数必须出现在非默认参数的右侧,且默认参数必须给定默认值
14.2.5 隐式转换和explicit
在调用构造函数时,有可能会调用隐式转换。
可以要求构造函数不进行隐式转换,在函数声明中带有explicit(显式),则该构造函数只能用于初始化和显式类型转换
explicit 构造函数名(形参列表);
- 在类进行实例化调用explicit多参数构造函数时,若带有=号,则隐式类型转换失效(含有等号,表示进行了隐式初始化;省略等号,为显式初始化为直接初始化)
- 调用explicit单参数构造函数,也只可以使用显式转换
- 在调用explicit构造函数时,使用{}进行初始化可以避免进行隐式类型转换而出错
14.2.6 构造函数初始化列表
调用构造函数时,可以初始化成员变量的值(为冒号括号逗号式写法),位于构造函数的定义中,(这种写法只用于构造函数中)
例如:
Time::Time(int tmphour, int tmpmin, int tmpsec)
:Hour(tmphour),Minute(tmpmin) //构造函数初始化列表
- 在书写带初始化列表的构造函数时,应避免某个成员变量(Minute)的值依赖某个成员变量的值(Hour),不可写成(Minute(Hour))
- 使用构造函数初始化列表可以提升初始化效率(避免了调用成员变量相关类的各种特殊成员函数)
14.3 inline、const、mutable、this与static
14.3.1 在类定义中实现成员函数inline
在类的定义中实现的成员函数会被当做inline内联函数处理
跳转至该位置
14.3.2 成员函数末尾的const
在成员函数的末尾添加const为常量成员函数,表示该成员函数不会修改该对象内任何成员变量的值(不可以在该成员函数的实现中修改成员变量的值)
对于成员函数和实现代码分开的成员函数,不但要在成员函数的声明中增加const,也要在实现中增加const
14.3.3 mutable
被mutable修饰的成员变量,永远为可变状态,可被const常量成员函数修改
14.3.4 返回自身对象的引用——this
1 | 声明: |
this在成员函数中为一个隐藏的函数参数,表示的时只想本对象的指针,而*this表示该指针指向的对象,即为本对象,*this为调用这个成员函数的对象
- 编译器内的实际声明:
Time& Time::rtnhour(Time * const this, int tmphour){…}
- 编译器内的实际调用:
mytime.rtnhour(&mytime,3);
this为一个指针常量,指向这个对象的本身,不可以再指向其他地方
跳转至该位置
- this只可以在成员函数(普通成员函数以及特殊的成员函数中使用),不可以在全局函数、静态函数中使用this指针
- 在普通成员函数中,this是一个非const对象的指针常量
- 在const成员函数中,this指针是指向一个const对象的指针常量
14.3.5 static成员
static成员变量:不属于某个对象而属于整个类
-
通过对象名修改了static成员变量的值,其他类对象中的相应值也发生改变
-
普通成员变量在定义类对象时被分配内存,==静态成员变量在
-
声明:在类内部声明
static 类型 变量名;
static 返回类型 函数名(形参列表);
- 初始化:(一般写在.cpp文件开头)
//[]内内容可以省略,初始化时可不给初值,且实现时不需要写static关键字
类型名 类::变量名 [= 值];
对象名.变量名 [=值];
类型名 类::静态成员函数名
对象名.静态成员函数名
14.4 类内初始化、默认构造函数、=default、=delete
14.4.1 类相关非成员函数
与类有点关系,但不适合写在类内的函数,这种函数的定义可以放到该类成员函数实现的代码中
14.4.2 类内初始值
在C++11新标准中,可以为新成员提供一个类内的初始值,那么在创建成员的时候,这个初始值就会初始化该成员变量,对于没有初始值的成员变量,系统会默认赋值。
若在构造函数初始化列表或者在构造函数中赋值,该值会覆盖初始值
14.4.3 const成员变量的初始化
对于类的const成员,只能使用初始化列表进行初始化操作(或者在声明该const变量时进行初始化操作),不能在构造函数内部进行赋值操作
- 构造函数不可以声明为const类型
- 在const变量完成初始化之后,该变量才具有const属性
14.4.4 默认构造函数
- 编译器会为一个类生成默认构造函数;若声明了一个构造函数,就不会自动生成其他的默认构造函数
- 一旦程序员书写了自己的构造函数,那么在创建对象时,必须提供与书写的构造函数形参相符合的实参,才能够成功创建对象
14.4.5 =defalue;和=delete
-
=default
相当于为特殊函数自动生成函数体(等价与空函数体{})
包括构造函数、拷贝构造函数等 -
=delete
显式的禁止某个函数
14.5 拷贝构造函数
跳转至该位置
默认情况下:类对象的复制就是每个成员变量的逐个复制
拷贝拷贝构造函数:用于类对象的复制
如果一个类的构造函数的第一个参数为所属类的类引用,若有额外的参数,那么这些额外的参数都有默认值,该构造函数的默认参数必须放在函数声明中(除非该构造函数没有函数声明),那么这个构造函数就是拷贝构造函数。
类名::类名(const 类名& 参数名,[其他形参列表]);
- const,拷贝构造函数的第一个参数都是带有const的
- explicit,(禁止隐式转换)
- 一般来说单参数的构造函数都声明为explicit以避免参数模糊不清的问题
- 拷贝构造函数,一般不声明为explicit
如果一个类没有自己的拷贝构造函数,那么编译器会合成一个拷贝构造函数,参考 跳转至该位置
拷贝构造函数的成员变量的赋值操作可以成初始化列表进行 跳转至该位置
调用拷贝构造函数的情况:
- 将对象作为实参传递给另一个非引用类型的形参(复制构造,效率低)
- 从函数中返回一个对象(系统会将局部对象(临时对象)return出去,会调用拷贝构造函数)
- 其他情况
14.6 重载运算符、拷贝赋值运算符与析构函数
14.6.1 重载运算符
运算符(==,!=,>,>=等)想要应用与类对象,需要对这些运算符进行重载(即以这些运算符为成员函数名定义成员函数实现功能)
实现方式:operator关键字后面接这个运算符(本质为函数,需要有返回类型和参数列表)
- 重载==运算符
1 | bool Time:operator==(Time& t) |
- 重载=运算符
1 | Time& Time:operator=(Time& t) |
- 重载运算符本质上是函数,函数的正式名字是operator关键字后面接这个运算符
- 如果一个类没有重载赋值,编译器可能会重载一个赋值运算符,可能不会重载运算符,其重载运算符只是简单将对象的成员变量的值复制到新对象对饮的成员变量中即完成赋值
- ==运算符,编译器不会默认重载
14.6.2 拷贝赋值运算符(赋值运算符)
- 给对象赋值,系统会调用一个拷贝构造赋值运算符,若不自己重载运算符,编译器会用默认的对象赋值规则为对象赋值,甚至在必要的情况下重载运算符
- 编译器重载的赋值运算符功能上只完成一些简单的成员变量赋值以及调用类类型成员变量所对应类中提供的拷贝赋值运算符
1 | // 赋值运算符重载 |
14.6.3 析构函数(释放函数)
- 对象销毁时,会调用析构函数,析构函数没有返回值
- 在构造函数new了一块内存,一般来说就应该写出自己的析构函数将new出来的内存释放(delete)
- 一个类只能有一个析构函数
1 | // 析构函数的定义 |
14.6.4 几个话题
- 构造函数的成员初始化
- 对于类类型成员变量的初始化,能放在构造函数的初始化列表里进行的,千万不要放在构造函数的函数体里进行,这样可以节省很多次不必要的成员函数调用,从而提高不少程序的执行效率