C++程序设计
C++程序设计
ZianHelloWorld
第一个程序:打印输出HelloWorld
1 |
|
#include
:引用头文件main
:程序的入口(主函数),一个项目只能有一个主函数std
:已经定义的命名空间,可释放注释,省略std1
cout << "Hello World!!" << endl;
cout
:C++中的输出类endl
:换行函数,可以释放缓存区
注释
单行注释
- 以两个斜线
//
开始,斜线之后的内容都是注释的内容,不会被编译1
// std::cout << "Hello World!!" << std::endl;
多行注释
- 以
/*
开始,以*/
结束,中间的所有内容都是注释的内容,可以换行,内容不会被编译1
2
3
4
5
6
7
8
9
10
using namespace std;
int main(){
/* std::cout << "Hello World!!" << std::endl;
std::cout << "Hello World!!" << std::endl; */
std::cout << "Hello World!!" << std::endl;
}
标识符
标识符:是由字母,数字,下划线,组成的一个字符序列,用来表示程序中的数据
1 | x = 10 // x代表10 |
标识符的命名规范
- 由字母,数字,下划线组成,不能有其他的组成部分
- 不能以数字开头,需要以字母或者下划线组成
- 不能与系统关键字重复
- 区分大小写
数据类型
整型
整型:就是整数的类型,描述的是整数数字
数据类型 | 关键字 | 空间大小 | 数据范围 |
---|---|---|---|
短整型 | short | 2字节 | [2^-15,2^15-1] |
整型 | int | 4字节 | [2^-31,2^31-1] |
长整型 | long | windows:4字节 | [2^-31,2^31-1] |
长长整型 | long long | 8字节 | [2^-63,2^63-1] |
sizeof()
:返回一个数据类型或者一个变量的空间占用大小1
2
3
4
5
6int main() {
int a = 10;
cout << sizeof(a) << endl; // 4
cout << sizeof(int) << endl; // 4
return 0;
}
浮点型
浮点型:就是小数的类型,用来描述的是小数数字
数据类型 | 关键字 | 空间大小 | 精确范围 |
---|---|---|---|
单精度浮点型 | float | 4字节 | 小数点后面7位 |
双精度浮点型 | double | 8字节 | 小数点后面15位 |
布尔型
用来描述非真即假,非假及真的数据类型
数据类型 | 关键字 | 空间大小 | 值 |
---|---|---|---|
布尔型 | bool | 1字节 | true | false |
字符型
用来描述一个文本内容中最小组成单位
数据类型 | 关键字 | 空间大小 | 值 |
---|---|---|---|
字符型 | char | 1字节 | 单引号'' 引起来的内容 |
字符串型
由若干个字符组成的一个有序的字符序列
数据类型 | 关键字 | 空间大小 | 值 |
---|---|---|---|
字符串型 | std::string | 单引号"" 引起来的内容 |
变量和常量
变量
变化的量,可以修改值
定义变量的语法:
数据类型
标识符(变量名)
数据类型
标识符
=值
数据类型
标识符1[= 值]
,标识符2[= 值]
,标识符3[= 值]
…
1 | int a; |
常量
不变化的量,不可以修改值
定义常量的语法:
const
数据类型
标识符(变量名)
1 | const int a = 10; |
转义字符
转义字符
\
- 配合某些特殊字符使用,使其变成普通字符
1
std::string str = "Hello \" World"; // 输出:Hello " World
- 配合某些特定的普通字符使用,代表某些特殊含义
\t
:制表符 tab 四个空格\n
:换行符\r
:回车符
控制台输入
cin
:读取控制台上输入的内容,并且给某一个变量进行赋值
1 | int num = 0; |
1 | int n1 = 0,n2 = 0,n3 = 0; |
1 | int main(){ |
缓冲区问题:
在控制台输入的内容都被暂存到了一个缓冲区
中,cin
从缓冲区
取数据给变量进行赋值
遇到.
和空格
会被忽略,可使用cin.ignore()
来忽略缓冲区的内容
1 |
|
可以连续输入
1 | int main() { |
错误处理
- cin内部会维护一个状态,来记录本次的读取操作是否正常
- cin.good();cin读取状态正确 值为1,否则为0
- cin.fail():cin读取状态错误 值为1,否则为0
- 如果被标记为
fail
状态,则会影响后续的读取操作cin.clear()
:恢复状态,清除错误状态
1 |
|
宏定义
宏定义:在文件的头部,使用
#define
来定义一个标识符,用来描述一个字符串,这个字符串就成为宏定义,使用时会自动替换为定义的值
1 |
|
命名空间
为了避免各种类库的命名的标识符冲突,C++引入了关键字
namespace
(命名空间/名字空间/名称空间),可以更好的控制标识符的作用域
使用语法
定义:
namespace
命名空间名
{ … };
使用:命名空间名
::成员名
1 |
|
命名空间是开放的,可以随时向一个命名空间添加成员
1 | namespace A{ |
using关键字
直接引用
指定的命名空间 或指定空间的指定成员
- 导入命名空间:
using
namespace
命名空间
- 导入命名空间的成员:
using
命名空间
::成员
1 | int main(){ |
注意事项:
- 如果引用的命名空间存在和当前的命名空间相同的成员,
默认使用当前的命名空间中的成员 - 如果引用的多个命名空间中存在相同名字的成员,且当前命名空间中没有这个成员,此时出现二义性。
命名空间不能省略
1 | namespace constant1{ |
运算符
算术运算符
对数字类型(整型,浮点型,字符型)的数据进行运算
运算符 | 含义 | 示例 |
---|---|---|
+ | 对两个数字进行相加的计算 | 10 + 3 = 13 |
- | 对两个数字进行相减的计算 | 10 - 3 = 7 |
* | 对两个数字进行相乘的计算 | 10 * 3 = 30 |
/ | 对两个数字进行相除的计算 | 10 / 3 = 3 |
% | 对两个数字进行求模的计算(求余数) | 10 % 3 = 1 |
++x | 前自增:x先进行+1,再进行运算 | y = ++x;x = x+1,y = x + 1 |
x++ | 后自增:再进行运算,x先进行+1 | y = x++;y = x,x = x + 1 |
–x | 前自减:x先进行-1,再进行运算 | y = –x;x = x-1,y = x - 1 |
x– | 后自增:再进行运算,x先进行-1 | y = x–;y = x,x = x - 1 |
注意事项:
- 整型与整型计算的结果,还是一个整型,所以如果10/3,得到的结果是浮点型3.33333,此时系统会将这个数字强制类型转换成整型的结果,
舍去小数点后面的所有数字 ,只保留整数部分3- 在进行计算的时候,结果会进行类型提升,
将结果提升为取值氛围大的数据类型
- int 与 int 的计算结果是 int
- int 与 long 的计算结果是 long
- float 与 long 的计算结果是 float
- float 与 double 的计算结果是 double
1 | int main(){ |
赋值运算符
将等号
=
右边的值赋给左边的变量
下面表格的前提:int num = 10;
运算符 | 示例 | 运算结果 |
---|---|---|
= | num = 10 | (num = 10) => 10 |
+= | num += 10 | num = (int)(num +10) |
-= | num -= 10 | num = (int)(num -10) |
*= | num *= 10 | num = (int)(num *10) |
/= | num /= 10 | num = (int)(num /10) |
%= | num %= 10 | num = (int)(num %10) |
关系运算符
对两个变量进行大小关系的比较,最后比较的结果一定是布尔类型的
运算符 | 示例 | 运算结果 |
---|---|---|
< | 10 < 20 | true |
> | 10 > 20 | false |
<= | 10 <= 20 | true |
>= | 10 >= 20 | false |
== | 10 == 20 | false |
!= | 10 != 20 | true |
逻辑运算符
对两个布尔类型的变量进行的逻辑操作
运算符 | 描述 | 示例 |
---|---|---|
& | 与运算,两边为真即为真,任意一个为假,结果即为假 | true & true => true |
| | 或运算,两边为假即为假,任意一个为真,结果即为真 | true | false => true |
! | 非运算,非真即假,非假及真 | !true => false |
^ | 异或运算,相同为假,不同为真 | true ^ true => false |
&& | 短路与,左边的结果为假,右边的表达式不参与运算 | false && true => true |
|| | 短路或,左边的结果为真,右边的表达式不参与运算 | true || false => true |
位运算符
作用于两个整数数字的运算,将参与运算的每一个数字计算出补码,对补码中的每一位进行类似于逻辑运算的操作,1相当于True,0相当于False
运算符 | 描述 | 示例 |
---|---|---|
& | 位与运算 | 10 & 20 |
| | 位或运算 | 10 & 20 |
^ | 位异或运算 | 10 & 20 |
~ | 按位取反运算 | ~10 |
<< | 位左移运算 | 10 << 1 => 10 * 2 |
>> | 位右移运算 | 10 >> 1 => 10 / 2 |
三目运算符
语法:
condition(条件)
?
value1
:
value2
condition
:是一个bool类型的变量或者bool类型运算结果的表达式
运算逻辑:如果condition
的值是true,三目运算符的结果取value1,否则取value2
1 | int main(){ |
流程控制
顺序结构
代码从上往下,依次执行
1 | int main(){ |
分支结构
程序在某一个节点遇到了多种执行的可能性,根据条件,选择一个分支继续执行
if-else
if else
语句:可用于变量的区间范围进行判断,根据结果选择分支继续执行
1 | if(条件判断1 true or false){ |
1 | int main(){ |
switch-case
switch case
语句:用于多重分支且条件判断是等值(固定特定值)判断的情况
1 | int main(){ |
- variable:确定的
字符
或整数
值- case:值只能是
字符
或整数
的字面量,不能是变量,值不允许重复- break:表示
跳出/结束
,结束switch语句- default:所有情况都不匹配,执行该处的内容,可以写在任意位置,也可以省略不写
1 |
|
switch case 语句中的的
穿透性
:
- 当switch的变量和某一个case的值匹配上之后,将会跳过后续的case或者default的匹配,直接向后穿透
- 为了避免switch的穿透性,每一个case和default可以使用
break
,来跳出switch语句
1 |
|
当然也可以利用switch的穿透性实现特定的功能
1 |
|
循环结构
某段代码需要被重复执行多次并且遵循一定规律,则使用循环结构
while循环
1 | while(条件表达式){ |
- 条件表达式:循环终止的判断条件语句,结果为bool类型的表达式
- 循环体:n行循环要执行的语句
流程说明:
- 执行条件表达式,也就是执行循环是否终止的判断条件,表达式的值如果是false,则循环结束,如果是true,循环继续执行
- 执行循环语句,大括号中的代码,需要循环的代码
- 回到第一步再次执行,直到表达式的结果为false,while循环才会结束
注意事项
- while循环本身没有循环变量的声明和初始化的部分,应在while循环前声明循环变量并赋值
- while循环本身也没有控制循环终止的判断条件语句部分,所以需要再循环体中增加相应的控制语句,否则容易死循环
1 | int main(){ |
示例:需要在控制台上输入一个整型数字,如果用户在控制台上输入的不正确,让用户重复输入,直到输入正确为止
1 |
|
do-while循环
1 | do{ |
流程说明
- 先执行循环体中的语句
- 执行条件表达式(循环终止的条件判断语句),结果如果为
true
,继续执行,如果是false
,则循环结束- 回到第一步,再次执行,直到条件表达式的结果为
false
注意事项
- do-while循环为先执行后判断,先执行一次循环体中的代码,然后再执行条件表达式,所以do-while循环至少执行一次
- 其他特点跟while循环一样
1 | int main(){ |
for循环
1 | for(循环起点;循环条件;循环步长){ |
- 循环起点:循环变量的
初始化
,如 int i = 0- 循环条件:循环
终止
的条件,为布尔表达式, 如 i < 10- 循环步长:循环改变的控制条件语句,如 i++
- 循环体:循环要执行的语句
- 表达式之间要用分号
;
分隔
流程说明
- 第一步:执行循环变量初始化语句(循环起点)
- 第二步:执行循环终止的判断条件表达式,结果为
ture
,继续执行第三步,结果为false
,结束循环- 第三步:执行循环语句
- 第四步:执行循环步长,也就是循环改变的控制条件语句,使循环变量的值发生改变
- 第五步:回到第二步,再次执行执行第二步到第五步,直到第二步的循环条件的表达式结果为
false
,循环结束
1 | int main(){ |
for循环的小括号中每一个部分都可以省略不写,但是分号
;
不能省略
1 | int main(){ |
流程控制的关键字
break
:
- 用于终止某个语句块的执行
- 如果是在循环中,则是跳出所在的循环,如果是在switch语句中,则为跳出所在的switch语句
1 | int main(){ |
continue
:
- 跳过本次循环,执行下一次循环,(如果有多次循环,默认继续执行离自己最近的循环)提前终止本次循环
- 只能在循环语句中使用
1 | int main(){ |
goto
:
- 可以在任意的位置设置
标签
,使用关键字goto
可以直接跳转到指定的标签
的位置继续执行
1 | int main(){ |
函数
函数是一个可以多次使用的功能代码块
函数的定义
1 | // 函数的定义 |
- 返回值:表示函数执行的结果,无返回值为
void
,有返回值就为对应的数据类型,return
返回函数执行后的结果,并结束函数的执行- 函数名:遵循标识符的命名规则
- 参数列表:定义若干个
参数
的部分,(参数类型 参数名1,参数类型 参数名2,参数类型,参数名3……)- 函数体:函数的
功能
实现部分
1 | // 取两数最大的值 |
注意事项
- 函数不能嵌套
- 函数不调用,函数就不会被执行
- 函数定义写在
main
方法前面,但可以提前声明变量名- 函数的执行会压到栈中去执行:
先进先出
,先调用的函数,在栈的底部存放,而新调用的函数,会在栈的顶部存放,程序先处理栈顶的函数中的逻辑- 函数的调用:如果函数定义有参数列表,则调用时,
参数个数
和参数类型
必须一致,调用时必须为每个参数赋值- 形参:在定义函数的时候,小括号中定义的参数,由于这样的参数只有形式上的定义,并没有具体的值,因此被称为形式
- 实参:在调用函数的时候,小括号中定义的参数,由于这样的参数,位形参提供了确切的值,因此将这样的参数叫做
实际参数
- 传参:在调用的函数的时候,用实参给形参赋值,这样的过程叫做
传参
- 参数可以使用
const
来修饰,表示参数的值不允许修改
的- 在定义函数的时候,可以给参数一个默认值,但该参数要放到参数列表的
末尾
,传参时可以赋值,也可以不赋值
函数的重载 OverLoad
在一个类中的多个函数,
函数名相同
,参数列表不同
(类型和数量不同),则这两个函数就构成了重载关系
1 | int add(int num1,int num2){ |
函数的递归
- 递归:一种程序设计的思想,在解决问题的时候,可以将问题拆分成若干个小问题,这些小问题的解决方式,与大的问题解决方式相同,通过解决这些小问题,逐渐解决这个大问题
- 由于涉及到方法的循环调用,因此容易出现
死递归
的情况,即所有的方法调用没有出口,只能将方法压栈执行,但是无法结束方法,因此在使用递归的时候,需要设置有效的出口条件
,避免无穷递归 - 递进:每一次推进,计算都比上一次变得简单,直至简单到无需继续推进,就能获得结果,也叫到达出口
- 回归:基于出口的结果,逐层向上回归,一次计算每一层的结果,直至回归到最顶层
1 | // 计算一个数字的阶乘,参数是需要计算阶乘的数字,返回值是计算的结果 |
1 | // 计算1 + 2 + 3 + ... + n的和 |
调用其他文件中的函数
.cpp
文件中的内容是无法跨文件
直接访问的, 若需要让某一个函数跨文件访问,需要为其定义一个.h
文件,称为头文件
- 在
头文件
中添加函数的声明部分
即可,需要使用的时候,直接使用#include
来包含指定的头文件
即可完成
1 | // tools.cpp文件 |
1 | // tools.h文件 |
1 | // main.cpp文件 |
指针与引用
内存分析
内存分区
在程序执行的时候,会在内存中开辟一些空间,存储数据,而内存又可以分为:
栈区
,堆区
,全局区
,代码区
四个区
代码区
代码区存放程序编译之后生成的
二进制代码
,例如我们写的函数就是存储在这。PS;函数在程序编译后,存储于代码区
,调用函数时,会压到栈区
执行其中的代码
全局区
全局区内的变量在程序
编译
阶段已经分配好内存空间
并初始化
,这块内存在程序的整个运行期间都会存在,主要存放静态变量
,全局变量
,全局常量
1 | using namespace std; |
栈区
栈区由
系统
进行的内存管理,主要存放函数的参数
以及局部变量
,在函数完成执行,系统会自动释放栈区
的内存,不需要用户管理
堆区
堆区就是通过
new
、malloc
、realloc
分配的内存块,由程序员
手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序执行期间,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。
内存中的数据残留
所谓的
删除数据
,删除的只是你对指定地址范围空间的使用权
,你不能够再去使用这块空间了,但是这块空间中原来的内容是不会被删除掉的!
指针
- 定义变量就是在
内存
中开辟了一块指定大小的空间,空间开辟的大小取决于不同的数据类型
所占用的空间大小。并且可以在这样的空间中进行值的赋值- 指针:每一个开辟中的内存空间,都是有一个
唯一
的地址
的,而这样的地址我们就称为是指针
&
:取地址操作符,取出变量的内存地址
*
:间接寻址符:返回变量所指定地址的变量的值
指针的定义
定义:
数据类型``*
指针名
1 | //声明了一个普通变量 a |
指针的类型
1 | int main(){ |
- 明明都是地址,为什么区分指针类型?因为
不同类型的指针±整数所跳过的字节数不同 int*
类型的指针 + 1 是跳过四
个字节double*
类型的指针 + 1 是跳过八
个字节char*
类型的指针 + 1 是跳过一
个字节指针
±整数
;是指针向前/向后移动的大小
(指针指向变量类型大小 * 整数)指针
-指针
:结果是两个指针之间所隔的元素个数
,这种操作通常用于计算数组中两个元素之间的距离
。
- 指针的作用就是通过
地址
取访问指针指向的变量
。- 指针的类型决定了指针解引用能够访问的
字节数
。- 例如上面的
int*
类型的指针能访问四个字节,double*
类型的指针可以访问八个字节,char*
类型的指针能够访问一个字节
空指针
空指针:指的是没有存储任何内存地址的
指针变量
,一般使用NULL
来表示一个空的地址。通常情况下,使用空指针可以对指针变量进行初始化
。
1 | // 这里的指针变量p没有存储任何的地址,就是一个空指针。 |
野指针
野指针:指针中存储有一个内存地址,但是这个地址指向的空间已经不存在了,这样的指针称为野指针
1 | // 这里定义了一个指针变量,随便写了一个地址,这个地址对应的空间极有可能是不存在的 |
常量指针
const
放在*
之前,表示常量指针,即常量的指针- 指针的指向是可以修改的,但是不能通过指针来修改指向
空间的值
1 | int num1 = 100; |
指针常量
const
放在*
之后,表示指针常量,即指针是一个常量值- 可以通过指针来修改指向空间的值,但是不能修改指针的
地址指向
1 | int num1 = 100; |
1 | void interchange(int* n1,int* n2){ |
引用
- 变量名实质上是一段连续内存空间的别名
- 引用可以作为一个已定义变量的
别名
,通过这个别名和原来的名字都能够找到这份数据- 定义:
数据类型
&
引用名
1 | int main(){ |
注意事项:
&
在此不是求地址运算,而是起标识
作用- 类型标识符是指
目标变量
的类型- 引用必须在定义的同时
初始化
,并且以后也要从一而终,不能再引用其它数据,有点类似于const
变量- 引用初始化之后不能改变
- 不能有
NULL
引用。必须确保引用是和一块合法的存储单元关联- 可以建立对
数组
的引用
引用的本质
所谓的引用,其实本质来讲就是一个
指针常量
1 | int main() { |
常量引用
常量引用,就是对一个常量建立引用,又称为
常引用
。主要用在函数的形参部分,访问误操作导致在函数题中通过形参,修改实参的值
1 | void change(const int& n) { |
数组
数组(array)是一种线性数据结构,其将相同类型的元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的索引(index)
数组:是一个数据容器,可以存储一个固定大小的相同类型
元素的顺序集合
特征:
- 数组可以用来存储任意数据类型的数据,但是所有的数据需要是
相同
的数据类型- 数组是一个定长的容器,一旦初始化完成,长度将
不能改变
- 数组中的元素被存储在一段
连续
的内存空间中元素
: 数组中存储的每一个数据,称为数组中的元素长度
: 数组的容量,即数组中可以存储多少个元素遍历
: 依次获取数组中的每一个元素
数组的定义
数据类型
数组名
[数组长度
]数据类型
数组名
[数组长度
] = {元素1,元素2,元素3……}数据类型
数组名
[] = {元素1,元素2,元素3……}
1 | // 1. 定义指定长度的数组,此时数组中填充的元素是不安全的 |
数组的使用
数组的访问:
为了能够区分数组中存储的每一个元素,在数组中存储的每一个元素都有一个唯一的序号
,称为下标
。我们在访问数组中的元素的时候,通过下标来访问。
- 注意事项: 数组中元素的下标是从
0
开始的!即数组中的元素下标范围是[0, 数组长度-1]
- 访问:
数组名
[下标
]- 修改:
数组名
[下标
] = 值sizeof()
:返回一个对象或者类型所占的内存字节数
- 注意事项: 通过下标访问数组元素的时候,
注意不要越界!
1 | // 定义一个数组 |
数组的内存分析
数组是一个容器,在内存中进行空间开辟的时候,并不是一个整体的空间,而是开辟了若干个
连续
的空间
例如:int array[10]
这个数组的长度为10,存储的元素数据类型是int。也就是说,需要在内存中开辟连续的10个4字节空间来存储元素。
array表示的是数组中首元素
的内存地址
!
int arr[10] = {1, 2, 3, 4, 5};
- arr表示的是数组中
首元素
的内存地址
。- 可以直接通过
*arr
来找到数组中的首元素
- 后面一个元素,可以通过arr的内存地址+4来访问到,再后面的一个元素,再加一个4….
(为什么要加4呢?因为这个数组中存储的元素类型是int,占据4个字节空间。如果是一个short数组,那就需要+2了)- C++将这一个元素的内存占用空间大小作为一个
单位
例如: int数组,一个单位就是4个字节,short数组,一个单位就是2个字节
在进行元素访问的时候,首元素直接通过*arr
就可以访问
后面的一位元素,偏移一个单位的地址*(arr + 1)
个单位
再后面的一位元素,偏移两个单位的地址*(arr + 2)
个单位
再后面的一位元素,偏移三个单位的地址*(arr + 3)
个单位
这就是为什么,数组中元素的下标是从0开始的- 注意事项:当数组作为参数传递到一个函数中的时候,传递的只是
首元素
的地址
!
1 | int main(){ |
1 | // 这里的参数其实等价于 int* arr,只是一个指向首元素的地址 |
数组的遍历
下标遍历
1 | int arr[10] = {1, 2, 3, 4, 5, 6}; |
范围遍历
1 | int arr[10] = {1, 2, 3, 4, 5}; |
数组的排序
选择排序
开启一个循环,每轮从未排序区间选择
最小
的元素,将其放到已排序区间的末尾
设数组的长度为
n
- 初始状态下,所有元素未排序,即未排序(索引)区间为
[0,n-1]
- 选取区间
[0,n-1]
中的最小元素,将其与索引0
处的元素交换。完成后,数组前 1 个元素已排序- 选取区间
[1,n-1]
中的最小元素,将其与索引1
处的元素交换。完成后,数组前 2 个元素已排序。- 以此类推。经过
n-1
轮选择与交换后,数组前n-1
个元素已排序。- 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。
1 |
|
冒泡排序
从数组最左端开始向右遍历,依次
比较相邻元素大小 ,如果左元素
>右元素
就交换二者。遍历完成后,最大的元素会被移动到数组的最右端
设数组的长度为
n
- 首先,对
n
个元素执行冒泡
,将数组的最大元素交换至正确位置。- 接下来,对剩余
n-1
个元素执行冒泡
,将第二大元素交换至正确位置。- 以此类推,经过
n-1
轮冒泡
后,前n-1
大的元素都被交换至正确位置。- 仅剩的一个元素必定是最小元素,无须排序,因此数组排序完成。
1 |
|
数组元素查找
数组元素查找指的是从给定的一个数组中查询
指定元素
出现的下标
由于需要查询的元素在指定的数组中可能出现多次,在这里我们只需要找到一个即可
顺序查询法
顺序查询,
就是从前往后遍历数组 ,将数组中的每一个元素和需要查询的元素进行对比。如果比较结果是相同的,说明找到了需要查询的元素。
1 |
|
二分查询法
- 二分查询, 即利用
数组中间的位置 , 将数组分为前后两个子表
。- 如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表
- 重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功
注意:二分查询, 要求数组必须是
排序
的, 否则无法使用二分查询
1 |
|
数组的练习
- 设计一个函数,找出一个数组中最大的数字,连同所在的下标一起输出。
1 | void printMaxElementAndIndex(int* array, int len) { |
- 设计一个函数,判断一个数组是不是一个升序的数组。
1 | bool checkAscending(const int* array, int len) { |
1 | bool checkAscending(const int* array, int maxIndex) { |
- 设计一个函数,找出一个整型数组中的第二大的值。
- 不可以通过排序实现,不能修改数组中的数据顺序
- 要考虑到最大的数字可能出现多次
1 | int getSecondMax(const int* array, int len) { |
- 设计一个函数,将一个数组中的元素倒序排列(注意,不是降序)。
1 | void reverse(int* array, int len) { |
- 将一个数组中的元素拷贝到另外一个数组中。
1 | void copy(int* src, int srcLen, int* dst, int dstLen) { |
- 设计一个函数,比较两个数组中的元素是否相同(数量、每一个元素都相同,才认为是相同的数组)。
1 | bool equals(int* array1, int arr1Len, int* array2, int arr2Len) { |
浅拷贝与深拷贝
有时候我们对数组进行操作的时候,需要进行数组的拷贝,而此时会有浅拷贝
和深拷贝
两种数组的拷贝形式。
- 浅拷贝:也就是
地址拷贝
,拷贝到的是数组的首元素地址
。 - 深拷贝:定义一个
新的数组
,长度与原来的数组相同,将原来数组中的每一个元素依次拷贝到新的数组中 。
从上述的说明中,可以看出,所谓的浅拷贝其实就是拷贝了一个地址,得到的数组与原来的数组指向的其实是同一块空间。因此对一个数组进行的操作都会对另外一个数组产生影响。而深拷贝则不然,深拷贝是创建了一个全新的数组,虽然元素与原来的数组元素相同,但是从内存上来看的话,这是一个全新的数组,修改这个数组不会对另外一个数组产生任何影响。
1 |
|
二维数组
二维数组的介绍
数组其实就是一个容器,存储着若干的数据。数组中可以存储
任意类型
的元素,可以存储整数
、可以存储浮点数字
、可以存储字符串
,其实数组中也可以存储一个数组
如果一个数组中存储的元素类型是一个
数组
,那么这样的数组就是二维数组
理论上讲还有三维数组、四维数组,只不过一般不去讨论。我们在讨论多维数组的时候,基本也就是指的二维数组了。
二维数组的定义
通常我们会将二维数组比作一个行列矩阵
,二维数组有多少元素,相当于有多少行。二维数组中的小一维数组有多少元素,相当于有多少列
二维数组的定义:
- 数据类型 标识符[
行数
][列数
];- 数据类型 标识符[
行数
][列数
] = { {val1, val2, val3}, {val1, val2, val3} };- 数据类型 标识符[
行数
][列数
] = { val1, val2, val3, val4 };- 数据类型 标识符[][
列数
] = { val1, val2, val3, … };
1 |
|
二维数组的使用
二维数组中的元素访问与一维数组是相同的,通过下标来进行访问即可!
1 | int main(){ |
面向对象
面向对象介绍
面向对象与面向过程
- 面向过程
- 是一种看待问题、解决问题的思维方式,着眼点在于
问题是如何一步步的解决的 ,然后亲力亲为的解决问题 - 面向对象
- 是一种看待问题、解决问题的思维方式,着眼点在于
找到一个能够帮助解决问题的实体,然后委托这个实体来解决问题
案例分析
小明买电脑
面向过程
- (小明)去市场买配件
- (小明)将零件运回家里
- (小明)将电脑组装起来
面向对象
- 找到一个能够帮助买电脑的朋友 – 老王
- 委托老王去买电脑配件
- 委托老王把电脑运回来
- 委托老王把电脑组装起来
把大象装冰箱
面向过程
- (我)打开冰箱门
- (我)把大象装进冰箱
- (我)关上冰箱门
面向对象
- 冰箱,开门
- 大象,进去冰箱里
- 冰箱,关门
类与对象
在面向对象的编程思想中,着眼点在于找到一个能够帮助解决问题的实体,然后委托这个实体解决问题。
在这里,这个具有特定的功能,对象
。
由若干个具有相同的特征和行为的对象的组成的集合
,就是一个类
。
类是对象的集合,对象是类的个体
类的设计与对象的创建
类的设计
从若干个具有相同的特征
和行为
的对象中,提取出这些相同的特征和行为,设计为一个类
类中定义所有的对象共有的特征
和行为
,其中,特征用属性
表示,行为用方法
表示
所谓属性
,其实就是定义在类中的一个变量
1 | // 类的定义 |
1 | // 设计一个类,描述人 |
注意事项:
- 在类中定义的属性、方法,默认都是
private
的权限,在类外是不能访问的。如果需要在类外访问,需要修改为public
权限。public
: 在任意位置
都可以访问protected
: 在当前类
和子类
中可以访问private
: 只能在当前类
中访问
对象的创建
1 | int main() { |
在上述的三种创建方式中,前两种方式是类似的。我们在创建对象的时候,区别主要是有没有使用关键字new。
使用new | 没有使用new | |
---|---|---|
内存方面 | 在堆空间开辟 | 在栈空间开辟 |
内存管理 | 需要手动使用delete销毁 | 不需要手动销毁 |
属性初始化 | 自动有默认的初始值 | 没有初始值 |
语法 | 需要用类*来接收变量 | 不需要使用* |
成员访问 | 通过.访问 | 通过->访问 |
成员访问
成员访问,即访问类中的成员(属性
、方法
)。
1 | int main() { |
1 | int main() { |
自定义的数据类型(类)
我们在定义类中的属性的时候,可以定义int
类型、float
类型、字符串
类型等等,那么能不能定义为另外的一个类的类型呢?
可以的!类其实就是一种自定义的复杂的数据类型。
1 | class Dog { |
类外和其他文件中实现类函数
类外实现
1 | class Person { |
其他文件中实现
如果我们设计的类需要在其他的文件中访问,需要设计
头文件
!
person.h
1 |
|
person.cpp
1 |
|
静态
我们在类中定义成员的时候(函数、属性),可以使用关键字static
来修饰,而这里的关键字static
表示的就是静态
在一个类中,被static
修饰的成员,称为静态成员
,可以分为: 静态属性
、静态函数
静态属性
静态的属性内存是开辟在
全局区
的,与对象无关,并不隶属于对象 。在程序编译的时候,就已经完成了空间的开辟与初始化的赋值操作了,并且在程序运行的整个过程中是始终保持的。
静态属性的空间开辟早于对象的创建 ,并且静态属性不隶属于对象,而是被所有的对象所共享的 。因此,如果你希望某一个属性是可以被所有的对象所共享的,就可以设置为静态的属性。
1 |
|
静态函数
被关键字
static
修饰的函数就是静态函数,与静态属性类似,静态函数依然不隶属于某一个对象,而是隶属于当前类
的。静态的函数可以使用对象来调用,也可以直接使用类来调用。
1 |
|
构造与析构
构造函数的介绍
构造函数,是一个比较特殊的函数。我们在使用一个类的对象的时候,需要为其分配空间。空间分配完成之后,我们一般都会对创建的对象的属性
进行初始化
的操作。而这个过程就可以在构造函数中来完成了。
因此: 构造函数是一个函数,是在对象创建的时候触发,用来对对象的属性进行初始化的赋值操作。
构造函数的定义
- 构造函数的名字,必须和
类的名字
相同! - 构造函数不能写
返回值
类型! - 构造函数可以有不同的
重载
!
1 | class Person { |
构造函数的使用
1 | // 构造函数的定义: |
explicit关键字
C++提供了关键字explicit
,禁止通过构造函数进行的隐式转换
。声明为explicit
的构造函数不能在隐式转换中使用。
用来修饰构造函数的
修饰符
,表示无法通过隐式调用来访问这个构造函数
1 | class Person { |
构造函数注意事项
- 如果在一个类中,没有手动写任意的构造函数,此时系统会自动为其提供一个
public
权限的无参构造函数
- 如果在一个类中,写了任意的一个构造函数,此时系统将不再提供默认的无参构造函数。如果需要的话,需要自己书写
1 | class Person { |
构造函数初始化列表
我们自己书写构造函数,很大的一个用途就是对属性进行初始化
的赋值操作
,就像如下代码:
1 | class Person { |
在上述的代码中,无论是无参的构造函数还是有参的构造函数,我们的目的都是在创建对象的时候,为属性进行初始化的赋值操作。但是重复的这样的赋值有点麻烦,此时,C++为我们提供了初始化列表的方式,来对属性进行初始化的赋值操作。
1 | class Person { |
拷贝构造函数
拷贝构造函数是C++中的另外一种构造函数,这个构造函数也是可以由系统自动提供的。
如果我们没有给一个类写拷贝构造函数,系统会自动的生成一个默认的拷贝构造函数
,为每一个属性进行赋值。
如果需要在拷贝构造函数中实现自己的拷贝逻辑,需要自己书写拷贝构造函数。
系统默认的拷贝构造函数:
1 | class Person { |
自己实现拷贝构造函数:
1 | class Person { |
析构函数
我们将一个对象从空间开辟开始,到空间销毁结束,这样的过程称为是一个对象的一生,用
生命周期
来描述这样的过程。对象的生命周期的起点是构造函数
,而对象的生命周期的终点就是析构函数
。
- 析构函数,将会在对象被销毁之前自动调用。
- 析构函数也是可以由系统自动生成的。
1 | class Person { |
深拷贝与浅拷贝的问题
深拷贝与浅拷贝是一个老生常谈的问题,在数组的部分提到过,在面向对象部分也有这两个概念。在这里我们首先需要先区分一下什么是深拷贝,什么是浅拷贝。
浅拷贝:在拷贝构造函数中,直接完成属性的赋值操作。
深拷贝:在拷贝构造函数中,创建一个新的空间,使得属性中的指针指向这个新的空间。
浅拷贝案例
1 | class Person { |
深拷贝案例
1 | class Person { |
this指针
this是什么
在C++中,this是一个指针
,用来指向当前的对象
的!
在类中的成员函数的
参数列表
中默认有一个this
参数
1 | class Person { |
在上述代码中,类Person中有一个函数getAge,可以返回属性age的值。那么问题来了,一个类可以有多个对象的。而非静态的属性age是隶属于对象的。不同的对象的age,在内存中的空间肯定也是不同的。如何区分需要返回哪一个对象的age呢?
在一个类中,涉及到成员的访问的时候,非静态的成员访问,通常都会使用this
指针来访问。
1 | class Person { |
this不可省略的情况
绝大多数的情况下,在一个类的内部,this
指针都是可以省略不写的。但是有一种情况,this指针不能省略,必须要显式的写出来:
如果在一个函数中出现了与当前类的属性同名字
的局部变量
!为了区分局部变量还是属性,此时的this
指针不能省略。
1 | class Person { |
返回当前对象的函数
1 | class Point { |
空指针访问成员函数
在C++中,使用空指针是可以访问成员函数的,但是需要注意:不能在函数中出现this
指针!
1 | class Person { |
常函数与常对象
什么是常函数
- 使用关键字
const
修饰的函数,叫做常函数。 - 常函数中,不允许
修改
属性的值。 - 常函数中,不允许
调用
其他的普通函数。 - 如果想要在常函数中修改某个属性的值,需要将这个属性设置为
mutable
。
1 | class Person { |
常对象
- 在对象创建的时候,使用
const
修饰的对象,就是常对象 - 常对象可以访问任意的属性值,但是不能修改
普通属性
的值 - 常对象可以修改
mutable
属性的值 - 常对象只能调用
常函数
1 | int main() { |
友元
友元是什么
类的主要特点之一是数据隐藏
,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员
,怎么办?
解决方法是使用友元函数
,友元函数是一种特权函数,C++允许这个特权函数访问私有成员
程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
全局函数做友元
1 | class Home { |
成员函数做友元
1 | // 定义有这样一个类,但是类中的成员是看不到的 |
类做友元
1 | class Home; |
运算符重载
什么是运算符重载
运算符重载,就是对已有的运算符
重新进行定义 ,赋予其另一种功能,以适应不同的数据类型。
运算符重载(operator overloading)只是一种调用
的方式。
在c++中,可以定义一个处理类
的新运算符
。这种定义很像一个普通的函数定义
,只是函数的名字由关键字operator
及其紧跟的运算符
组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
语法:
定义重载的运算符就像定义函数,只是该函数的名字是operator@
,这里的@
代表了被重载的运算符
。函数的参数中参数个数取决于两个因素。
- 运算符是一元(一个参数)的还是二元(两个参数);
- 运算符被定义为
全局函数
(对于一元是一个参数,对于二元是两个参数)还是成员函数
(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数)
注意:
有些人很容易滥用运算符重载。它确实是一个有趣的工具。但是应该注意,它仅仅是一种语法上的方便而已,是另外一种函数调用的方式。从这个角度来看,只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比我们写代码多多了)才有理由重载运算符。如果不是这样,就改用其他更易用,更易读的方式。
可重载的运算符
几乎所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能改变运算符优先级
,不能改变运算符的参数个数
。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清语意。
可重载的运算符
+ | - | * | / | % | ^ | & | | | ~ |
---|---|---|---|---|---|---|---|---|
! | = | < | > | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= | <<= | == | != |
<= | >= | && | || | ++ | – | ->* | ‘ | -> |
[] | () | new | delete | new[] | delete[] |
不可重载的运算符
. | :: | .* | ?: | sizeof |
---|
运算符重载: +
1 | class Point { |
运算符重载: ++
1 | class Point { |
运算符重载: <<
1 | class Point { |
运算符重载: =
1 | class Person { |
封装
面向对象编程思想中,有三大特性:
封装
、继承
、多态
。
封装可以有广义和狭义上的概念。广义上的封装,我们可以将一些功能相近的一些类放入一个模块中。这里我们更多强调的是狭义上的封装性。
- 定义:我们可以通过对具体属性的封装实现.把对
成员变量
的访问
进行私有化
,让他只能在类内部可见
,通过公共的方法间接实现访问 - 优点:提高了代码的安全性,复用性和可读性.
1 | class Student { |
继承
程序中的继承
在现实生活中,我们与父母有继承
的关系,在java中也存在继承的思想,来提高代码的复用性、代码的拓展性。
程序中的继承,是类与类之间的特征和行为的一种赠予或获取。一个类可以将自己的属性
和方法
赠予其他的类,一个类也可以从其他的类中获取他们的属性和方法。
两个类之间的继承,必须满足 is a 的关系。
两个类之间,A类将属性和特征赠予B类。此时A类被称为是父类,B类被称为是子类,两者之间的关系是子类继承自父类
继承的语法
在C++中,在定义类的时候,类名后面使用冒号
来定义父类
。
- 类中的所有成员都可以继承给子类,但是私有的成员,由于访问权限的限制,子类无法访问。
- 一个类在继承了其他类之后,也可以被其他类
继承
。 - 使用继承,可以简化代码、提高代码的复用性、提高代码的拓展性,最重要的是让类与类之间产生了继承关系,是多态的前提。
1 | class Animal { |
继承的三种方式
在C++中,继承有三种方式,分别是:公共继承
、保护继承
和私有继承
。其实只是一个访问权限的问题。
- 公共继承:继承到父类中的属性,保留原本的访问权限(私有除外)
- 保护继承:继承到父类中的属性,超过
protected
权限的部分将降为protected
权限(私有除外) - 私有继承:继承到父类中的属性,访问权限都为
private
权限(私有除外)
C++中默认使用的是私有继承
!
公共继承
1 | // 定义类,分别定义三种访问权限的属性 |
保护继承
1 | // 定义类,分别定义三种访问权限的属性 |
私有继承
1 | // 定义类,分别定义三种访问权限的属性 |
继承中的构造和析构
子类对象在创建的时候,需要先调用父类中的构造函数,用来初始化父类部分。因此,子类对象创建的时候,先调用父类的构造函数,再调用子类自己的构造函数。
而析构函数的调用正好相反,先调用子类的析构函数,再调用父类的析构函数。
1 | class Animal { |
从上述的代码中可以看到,子类对象在创建的时候,需要先调用父类中的构造函数来构造父类部分。这里默认是调用父类中的无参构造函数。那么问题来了:如果父类中没有无参构造函数,或者父类中的无参构造函数是私有的,怎么办?
1 | class Animal { |
父类子类成员同名的情况
如果父类和子类中出现了同名字的成员(属性、函数),子类会将从父类继承到的成员隐藏起来。此时使用子类对象来访问的时候,默认访问的是子类中的成员。如果想要访问父类中的成员,需要手动指定作用域。
1 | class Animal { |
多继承
多继承语法
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
多继承会带来一些二义性的问题,如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
解决方法就是显示指定调用那个基类的版本。
1 | class Base1{ |
菱形继承
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:
羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
1 | class BigBase{ |
虚继承
Base1,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。
1 | class BigBase{ |
多态
多态的基本概念
什么是多态
生活中的多态,是指的客观的事物在人脑中的主观体现。例如,在路上看到一只哈士奇,你可以看做是哈士奇,可以看做是狗,也可以看做是动物。主观意识上的类别,与客观存在的事物,存在 is a
的关系的时候,即形成了多态。
在程序中,一个类的引用指向另外一个类的对象,从而产生多种形态。当二者存在直接或者间接的继承关系时,父类引用指向子类的对象,即形成多态。
多态是面向对象三大特性之一,记住继承是多态的前提,如果类与类之间没有继承关系,也不会存在多态。
多态的分类
c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
对象转型
对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
1 | class Animal { |
虚函数
上述代码的运行结果是: Animal bark。说明执行的是父类中的bark函数,而非子类中的函数。
于是现在就有一个问题出现了: 为什么?animal的引用指向的实际上是一个Dog对象,但是为什么会调用父类中的函数实现呢?
解决这个问题,我们需要了解下绑定(捆绑,binding)概念。
把函数体与函数调用相联系称为绑定(捆绑,binding)
当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding)。
上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于调用函数的时候使用的是Animal类型,编译器确定了应该调用的bark是Animal::bark的,而不是真正传入的对象Dog::bark。
解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。
C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)
C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。对于特定的函数进行动态绑定,C++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用。
- 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
- 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
- 在派生类中virtual函数的重定义称为重写(override).
- virtual关键字只能修饰成员函数.
- 构造函数不能为虚函数
1 | class Animal { |
多态案例
未使用多态实现
1 |
|
使用多态实现
1 | // 快递公司类 |
纯虚函数与抽象类
在设计程序时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
1 | 例如: |
纯虚函数使用virtual来修饰一个函数,并且实现部分直接设置为0。
1 | virtual void test() = 0; |
如果一个类中包含了纯虚函数,那么这个类也自动的编程了抽象类了。抽象类无法实例化对象,并且子类必须重写实现父类中的纯虚函数,否则子类也是抽象类。
1 | class TrafficTools { |
纯虚函数与多继承
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。
绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
注意:除了析构函数外,其他声明都是纯虚函数。
1 | // 定义一个功能集合,厨师类 |
虚析构函数
析构函数是对象生命周期的终点,在对象被销毁之前调用。在析构函数中,我们一般会进行资源的释放、空间的销毁的操作。例如,在一个类中有指向堆空间内存的指针,我们需要通过这样的指针来销毁对应的堆空间。但是,在多态中,父类的引用可以指向子类的对象,那么我们在使用父类的引用来销毁空间的话,就有可能会出现子类中引用的堆空间无法销毁的情况,造成内存泄漏。而解决方案就是为父类添加虚析构函数。
1 | class Animal { |
虚析构函数也可以做成纯虚析构函数,如果一个类中包含了纯虚析构函数,那么这个类依然是一个抽象类,无法实例化对象。
总结:
如果一个类的目的不是为了实现多态,仅仅是作为一个基类来使用,那么无需将析构函数设置为虚析构函数。
如果一个类的目的就是为了实现多态的,那么这个类的析构函数就有必要设置为虚析构函数。
结构体
结构体的定义与使用
在C++中,还有一种用户自定义的数据类型,结构体。结构体的定义与使用基本与类相同。
1 | // 定义结构体 |
结构体与类的区别
C++对结构体进行了很多的拓展,是的C++对结构体用于与类几乎相同的功能:可以设计属性、函数,可以设计构造、析构,甚至可以有继承,可以有多态。现在看来C++的结构体与类的区别,主要是一点:默认的访问权限不同
- 类成员默认的访问权限是private
- 结构体成员默认的访问权限是public
模板
模板的介绍
c++提供了函数模板(function template)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体制定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
c++提供两种模板机制:函数模板和类模板
总结:
模板把函数或类要处理的数据类型参数化,表现为参数的多态性,成为类属。
模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
函数模板
函数模板的定义
1 | // 需求:我想要设计一个函数,实现两个int变量的值的交换 |
1 | // 定义函数模板 |
函数模板的使用
1 | template<class T> |
函数模板案例
1 | // 需求:定义一个模板函数,实现对一个数组中对元素进行升序排序 |
函数模板与普通函数
函数模板和普通函数在调用的时候,需要注意:
- 普通函数调用,是可以发生自动的类型转换的;函数模板调用,是不可以发生自动的类型转换的
- 如果调用函数的时候,实参既可以匹配普通函数,又可以匹配函数模板,则优先匹配普通函数
1 | // 定义一个函数模板 |
函数模板的局限性
函数模板虽然很通用,但是并不是万能的,有时候也会有不适配的情况出现。
1 | template<class T> |
对于上述的函数模板来说,如果是比较整型、浮点型甚至字符类型的数据都是没有问题的。可是如果我设置为Person类型呢?两个Person对象无法使用>进行比较,这里自然也就出问题了。
那么如何解决这样的问题呢?
- 重载运算符,重载>运算符。
- 通过函数模板的重载来解决。
函数模板的重载,就是为了解决特定类型的对象的问题,通过函数模板的重载,可以为这些特定的数据类型提供具像化的模板。
1 | class Person { |
类模板
类模板的定义
类模板和函数模板的定义和使用基本是一样的,如何定义函数模板,就如何定义类模板。但是类模板与函数模板还是有点区别的:
- 类模板不能自动类型推导。
1 | template<class T1, class T2 = int> |
类模板做函数参数
1 | template<class T1, class T2 = int> |
类模板继承
1 | // 定义模板类 |
类模板中的成员函数创建时机
类模板中的成员函数在编译的时候是不会创建的,是在调用这个函数的时候创建。
1 | class Dog { |
类模板类外实现
1 | template<typename T, typename M> |
类模板头文件和原文件分离问题
我们在写程序的时候,很多时候都是需要将类的声明和实现分开来写。将类的声明部分写到.h文件中,将类的实现部分写在.cpp文件中。在使用到这个类的时候,直接包含.h文件即可。但是,如果是一个模板类,这样做是有问题的。
NumberCalculator.h
1 |
|
NumberCalculator.cpp
1 |
|
1 | int main() { |
类模板遇到友元
1 | // 全局友元函数类外实现-03:定义类 |
STL标准模板库
STL概述
长久以来,软件界一直希望建立一种可重复利用的东西,以及一种得以制造出”可重复运用的东西”的方法,让程序员的心血不止于随时间的迁移,人事异动而烟消云散,从函数(functions),类别(classes),函数库(function libraries),类别库(class libraries)、各种组件,从模块化设计,到面向对象(object oriented ),为的就是复用性的提升。
复用性必须建立在某种标准之上。但是在许多环境下,就连软件开发最基本的数据结构(data structures) 和算法(algorithm)都未能有一套标准。大量程序员被迫从事大量重复的工作,竟然是为了完成前人已经完成而自己手上并未拥有的程序代码,这不仅是人力资源的浪费,也是挫折与痛苦的来源。
为了建立数据结构和算法的一套标准,并且降低他们之间的耦合关系,以提升各自的独立性、弹性、交互操作性(相互合作性,interoperability),诞生了STL。
STL基本概念
STL(Standard Template Library,标准模板库),是惠普实验室开发的一系列软件的统称。现在主要出现在 c++中,但是在引入 c++之前该技术已经存在很长时间了。
STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator),容器和算法之间通过迭代器进行无缝连接。STL 几乎所有的代码都采用了模板类或者模板函数,这相比传统的由函数和类组成的库来说提供了更好的代码重用机会。STL(Standard Template Library)标准模板库,在我们 c++标准程序库中隶属于 STL 的占到了 80%以上。
STL六大组件简介
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器是一种class template。
算法:各种常用的算法,如sort、find、copy、for_each。从实现的角度来看,STL算法是一种function tempalte.
迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator* , operator-> , operator++,operator–等指针相关操作予以重载的class template. 所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。
仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class 或者class template
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
空间配置器:负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte.
STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。
STL优点
STL 是 C++的一部分,因此不用额外安装什么,它被内建在你的编译器之内。
STL 的一个重要特性是将数据和操作分离。数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作。程序员可以不用思考 STL 具体的实现过程,只要能够熟练使用 STL 就 OK 了。这样他们就可以把精力放在程序开发的别的方面。
STL 具有高可重用性,高性能,高移植性,跨平台的优点。
高可重用性:STL 中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。关于模板的知识,已经给大家介绍了。
高性能:如 map 可以高效地从十万条记录里面查找出指定的记录,因为 map 是采用红黑树的变体实现的。
高移植性:如在项目 A 上用 STL 编写的模块,可以直接移植到项目 B 上。
STL三大组件
容器
容器,置物之所也。
研究数据的特定排列方式,以利于搜索或排序或其他特殊目的,这一门学科我们称为数据结构。大学信息类相关专业里面,与编程最有直接关系的学科,首推数据结构与算法。几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL容器就是将运用最广泛的一些数据结构实现出来。
常用的数据结构:数组(array),链表(list),tree(树),栈(stack),队列(queue),集合(set),映射表(map),根据数据在容器中的排列特性,这些数据分为序列式容器和关联式容器两种。
- 序列式容器强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。
- 关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器
容器可以嵌套容器!
算法
算法,问题之解法也。
以有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms).
广义而言,我们所编写的每个程序都是一个算法,其中的每个函数也都是一个算法,毕竟它们都是用来解决或大或小的逻辑问题或数学问题。STL收录的算法经过了数学上的效能分析与证明,是极具复用价值的,包括常用的排序,查找等等。特定的算法往往搭配特定的数据结构,算法与数据结构相辅相成。
算法分为: 质变算法 和 非质变算法。
质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等
迭代器
迭代器(iterator)是一种抽象的设计概念,现实程序语言中并没有直接对应于这个概念的实物。iterator定义如下:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
迭代器的设计思维-STL的关键所在,STL的中心思想在于将容器(container)和算法(algorithms)分开,彼此独立设计,最后再一贴胶着剂将他们撮合在一起。从技术角度来看,容器和算法的泛型化并不困难,c++的class template和function template可分别达到目标,如何设计出两这个之间的良好的胶着剂,才是大难题。
迭代器的种类:
输入迭代器 | 提供对数据的只读访问 | 只读,支持++、==、!= |
---|---|---|
输出迭代器 | 提供对数据的只写访问 | 只写,支持++ |
前向迭代器 | 提供读写操作,并能向前推进迭代器 | 读写,支持++、==、!= |
双向迭代器 | 提供读写操作,并能向前和向后操作 | 读写,支持++、–, |
随机访问迭代器 | 提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器 | 读写,支持++、–、[n]、-n、<、<=、>、>= |
常用容器
string容器
string容器基本概念
C风格字符串(以空字符结尾的字符数组)太过复杂难于掌握,不适合大程序的开发,所以C++标准库定义了一种string类。
C++的字符串与C语言的字符串比较
- C语言:char* 是一个指针。
- C++:
- string是一个类,内部封装了char*,用来管理这个容器。
- string类中封装了很多的功能函数,非常实用。例如:find、copy、delete、replace、insert等。
- 不用考虑内存释放和越界的问题。
string管理char*所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。
string容器常用操作
string构造函数
1
2
3
4string();//创建一个空的字符串 例如: string str;
string(const string& str);//使用一个string对象初始化另一个string对象
string(const char* s);//使用字符串s初始化
string(int n, char c);//使用n个字符c初始化string基本赋值操作
1
2
3
4
5
6
7
8string& operator=(const char* s);//char*类型字符串 赋值给当前的字符串
string& operator=(const string &s);//把字符串s赋给当前的字符串
string& operator=(char c);//字符赋值给当前的字符串
string& assign(const char *s);//把字符串s赋给当前的字符串
string& assign(const char *s, int n);//把字符串s的前n个字符赋给当前的字符串
string& assign(const string &s);//把字符串s赋给当前字符串
string& assign(int n, char c);//用n个字符c赋给当前字符串
string& assign(const string &s, int start, int n);//将s从start开始n个字符赋值给字符串string存取字符操作
1
2char& operator[](int n);//通过[]方式取字符
char& at(int n);//通过at方法获取字符string拼接操作
1
2
3
4
5
6
7
8string& operator+=(const string& str);//重载+=操作符
string& operator+=(const char* str);//重载+=操作符
string& operator+=(const char c);//重载+=操作符
string& append(const char *s);//把字符串s连接到当前字符串结尾
string& append(const char *s, int n);//把字符串s的前n个字符连接到当前字符串结尾
string& append(const string &s);//同operator+=()
string& append(const string &s, int pos, int n);//把字符串s中从pos开始的n个字符连接到当前字符串结尾
string& append(int n, char c);//在当前字符串结尾添加n个字符cstring查找和替换
1
2
3
4
5
6
7
8
9
10int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos = 0) const; //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const; //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos = 0) const; //查找字符c第一次出现位置
int rfind(const string& str, int pos = npos) const;//查找str最后一次位置,从pos开始查找
int rfind(const char* s, int pos = npos) const;//查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const;//从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符c最后一次出现位置
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n, const char* s); //替换从pos开始的n个字符为字符串sstring比较操作
1
2
3
4
5
6
7/*
compare函数在>时返回 1,<时返回 -1,==时返回 0。
比较区分大小写,比较时参考字典顺序,排越前面的越小。
大写的A比小写的a小。
*/
int compare(const string &s) const;//与字符串s比较
int compare(const char *s) const;//与字符串s比较string子串
1
string substr(int pos = 0, int n = npos) const;//返回由pos开始的n个字符组成的字符串
string插入和删除操作
1
2
3
4string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n, char c);//在指定位置插入n个字符c
string& erase(int pos, int n = npos);//删除从Pos开始的n个字符string和C语言风格字符串转换
1
2
3
4
5
6//string 转 char*
string str = "itcast";
const char* cstr = str.c_str();
//char* 转 string
char* s = "itcast";
string str(s);1
2在c++中存在一个从const char到string的隐式类型转换,却不存在从一个string对象到Cstring的自动类型转换。对于string类型的字符串,可以通过cstr()函数返回string对象对应的C_string.
通常,程序员在整个程序中应坚持使用string类对象,直到必须将内容转化为char时才将其转换为C_string.
提示:
为了修改string字符串的内容,下标操作符[]和at都会返回字符的引用。但当字符串的内存被重新分配之后,可能发生错误.
1 | string s = "abcdefg"; |
vector容器
vector容器基本概念
vector的数据安排以及操作方式,与array非常相似,两者的唯一差别在于空间的运用的灵活性。Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,可以,一切琐碎得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就要求一个大块头的array了。
vector容器常用操作
vector的构造函数
1
2
3
4
5
6
7
8vector<T> v; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end());//将v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem);//构造函数将n个elem拷贝给本身。
vector(const vector &vec);//拷贝构造函数。
//例子 使用第二个构造函数 我们可以...
int arr[] = {2,3,4,1,9};
vector<int> v1(arr, arr + sizeof(arr) / sizeof(int));vector的常用赋值函数
1
2
3
4assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
vector& operator=(const vector &vec);//重载等号操作符
swap(vec);// 将vec与本身的元素互换。vector的大小操作
1
2
3
4
5
6size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(int num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem);//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长>度的元素被删除。
capacity();//容器的容量
reserve(int len);//容器预留len个元素长度,预留位置不初始化,元素不可访问。vector的数据存取操作
1
2
3
4at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。
operator[];//返回索引idx所指的数据,越界时,运行直接报错
front();//返回容器中第一个数据元素
back();//返回容器中最后一个数据元素vector插入和删除操作
1
2
3
4
5
6insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele.
push_back(ele); //尾部插入元素ele
pop_back();//删除最后一个元素
erase(const_iterator start, const_iterator end);//删除迭代器从start到end之间的元素
erase(const_iterator pos);//删除迭代器指向的元素
clear();//删除容器中所有元素
vector迭代器
Vector维护一个线性空间,所以不论元素的型别如何,普通指针都可以作为vector的迭代器,因为vector迭代器所需要的操作行为,如operator, operator->, operator++, operator–, operator+, operator-, operator+=, operator-=, 普通指针天生具备。Vector支持随机存取,而普通指针正有着这样的能力。所以vector提供的是随机访问迭代器(Random Access Iterators).
1 | // vector提供了begin()函数,用来返回指向首元素的指针 |
vector小案例
巧用swap收缩内存空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using namespace std;
int main(){
vector<int> v;
for (int i = 0; i < 100000;i ++){
v.push_back(i);
}
cout << "capacity:" << v.capacity() << endl;
cout << "size:" << v.size() << endl;
//此时 通过resize改变容器大小
v.resize(10);
cout << "capacity:" << v.capacity() << endl;
cout << "size:" << v.size() << endl;
//容量没有改变
vector<int>(v).swap(v);
cout << "capacity:" << v.capacity() << endl;
cout << "size:" << v.size() << endl;
system("pause");
return EXIT_SUCCESS;
}reserve预留空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using namespace std;
int main(){
vector<int> v;
//预先开辟空间
v.reserve(100000);
int* pStart = NULL;
int count = 0;
for (int i = 0; i < 100000;i ++){
v.push_back(i);
if (pStart != &v[0]){
pStart = &v[0];
count++;
}
}
cout << "count:" << count << endl;
system("pause");
return EXIT_SUCCESS;
}
deque容器
deque容器基本概念
Vector容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector容器也可以在头尾两端插入元素,但是在其头部操作效率奇差,无法被接受。
deque容器和vector容器最大的差异,一在于deque允许对头端进行元素的插入和删除操作。二在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,”旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque身上是不会发生的。也因此,deque没有必须要提供所谓的空间保留(reserve)功能.
虽然deque容器也提供了Random Access Iterator,但是它的迭代器并不是普通的指针,其复杂度和vector不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该尽可能的使用vector,而不是deque。对deque进行的排序操作,为了最高效率,可将deque先完整的复制到一个vector中,对vector容器进行排序,再复制回deque.
deque容器常用操作
deque构造函数
1
2
3
4deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将n个elem拷贝给本身。
deque(const deque &deq);//拷贝构造函数。deque赋值操作
1
2
3
4assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
deque& operator=(const deque &deq); //重载等号操作符
swap(deq);// 将deq与本身的元素互换deque大小操作
1
2
3
4deque.size();//返回容器中元素的个数
deque.empty();//判断容器是否为空
deque.resize(num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除。deque双端操作和删除
1
2
3
4push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据deque数据存取
1
2
3
4at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。
operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据deque插入操作
1
2
3insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。deque删除操作
1
2
3clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
deque小案例
有5名选手:选手ABCDE,10个评委分别对每一名选手打分,去除最高分,去除评委中最低分,取平均分。
1. 创建五名选手,放到vector中
2. 遍历vector容器,取出来每一个选手,执行for循环,可以把10个评分打分存到deque容器中
3. sort算法对deque容器中分数排序,pop_back pop_front去除最高和最低分
4. deque容器遍历一遍,累加分数,累加分数/d.size()
5. person.score = 平均分
stack容器
stack容器基本概念
stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,形式如图所示。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。
有元素推入栈的操作称为:push,将元素推出stack的操作称为pop.
stack是没有迭代器的:
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。
stack容器常用操作
构造函数
1
2stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式:
stack(const stack &stk);//拷贝构造函数赋值操作
1
stack& operator=(const stack &stk);//重载等号操作符
数据存取操作
1
2
3push(elem);//向栈顶添加元素
pop();//从栈顶移除第一个元素
top();//返回栈顶元素大小操作
1
2empty();//判断堆栈是否为空
size();//返回堆栈的大小
queue容器
queue容器基本概念
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。
queue容器没有迭代器:Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。
queue容器常用操作
构造函数
1
2queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式:
queue(const queue &que);//拷贝构造函数存取、插入、删除操作
1
2
3
4push(elem);//往队尾添加元素
pop();//从队头移除第一个元素
back();//返回最后一个元素
front();//返回第一个元素赋值操作
1
queue& operator=(const queue &que);//重载等号操作符
大小操作
1
2empty();//判断队列是否为空
size();//返回队列的大小
list容器
list容器基本概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
相较于vector的连续线性空间,list就显得复杂许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素的移除,list永远是常数时间。
List和vector是两个最常被使用的容器。
List容器是一个双向链表。
采用动态存储分配,不会造成内存浪费和溢出
链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
链表灵活,但是空间和时间额外耗费较大
list的迭代器
List容器不能像vector一样以普通指针作为迭代器,因为其节点不能保证在同一块连续的内存空间上。List迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。
由于list是一个双向链表,迭代器必须能够具备前移、后移的能力,所以list容器提供的是Bidirectional Iterators.
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。
list容器常用操作
构造函数
1
2
3
4list<T> lstT;//list采用采用模板类实现,对象的默认构造形式:
list(beg,end);//构造函数将[beg, end)区间中的元素拷贝给本身。
list(n,elem);//构造函数将n个elem拷贝给本身。
list(const list &lst);//拷贝构造函数。元素插入和删除操作
1
2
3
4
5
6
7
8
9
10
11push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素
insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
remove(elem);//删除容器中所有与elem值匹配的元素。大小操作
1
2
3
4
5
6
7
8
9size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(num);//重新指定容器的长度为num,
// 若容器变长,则以默认值填充新位置。
// 如果容器变短,则末尾超出容器长度的元素被删除。
resize(num, elem);//重新指定容器的长度为num,
// 若容器变长,则以elem值填充新位置。
// 如果容器变短,则末尾超出容器长度的元素被删除。赋值操作
1
2
3
4assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
list& operator=(const list &lst);//重载等号操作符
swap(lst);//将lst与本身的元素互换。数据存取操作
1
2front();//返回第一个元素。
back();//返回最后一个元素反转、排序
1
2reverse();//反转链表,比如lst包含1,3,5元素,运行此方法后,lst就包含5,3,1元素。
sort(); //list排序
set/multiset容器
set/multiset容器基本概念
Set的特性是。所有元素都会根据元素的值自动被排序。Set不允许两个元素有相同的值。
我们可以通过set的迭代器改变set元素的值吗?不行,因为set元素值就是其值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set的iterator是一种const_iterator.
set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。
multiset特性及用法和set完全相同,唯一的差别在于它允许值重复。set和multiset的底层实现是红黑树,红黑树为平衡二叉树的一种。
树的简单知识:
二叉树就是任何节点最多只允许有两个字节点。分别是左子结点和右子节点
二叉树示意图
二叉搜索树,是指二叉树中的节点按照一定的规则进行排序,使得对二叉树中元素访问更加高效。二叉搜索树的放置规则是:任何节点的元素值一定大于其左子树中的每一个节点的元素值,并且小于其右子树的值。因此从根节点一直向左走,一直到无路可走,即得到最小值,一直向右走,直至无路可走,可得到最大值。那么在二叉搜索树中找到最大元素和最小元素是非常简单的事情。下图为二叉搜索树:
上面我们介绍了二叉搜索树,那么当一个二叉搜索树的左子树和右子树不平衡的时候,那么搜索依据上图表示,搜索9所花费的时间要比搜索17所花费的时间要多,由于我们的输入或者经过我们插入或者删除操作,二叉树失去平衡,造成搜索效率降低。
所以我们有了一个平衡二叉树的概念,所谓的平衡不是指的完全平衡。
set/multiset容器常用操作
构造函数
1
2
3set<T> st;//set默认构造函数:
mulitset<T> mst; //multiset默认构造函数:
set(const set &st);//拷贝构造函数set赋值
1
2set& operator=(const set &st);//重载等号操作符
swap(st);//交换两个集合容器set大小操作
1
2size();//返回容器中元素的数目
empty();//判断容器是否为空插入和删除操作
1
2
3
4
5insert(elem);//在容器中插入元素。
clear();//清除所有元素
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(elem);//删除容器中值为elem的元素。查找操作
1
2
3
4
5find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
count(key);//查找键key的元素个数
lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器。
upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器。
equal_range(keyElem);//返回容器中key与keyElem相等的上下限的两个迭代器。
对组
对组(pair)将一对值组合成一个值,这一对值可以具有不同的数据类型,两个值可以分别用pair的两个公有属性first和second访问。
类模板:template <class T1, class T2> struct pair.
如何创建对组?
1 | //第一种方法创建一个对组 |
map/multimap容器
map/multimap基本概念
Map的特性是,所有元素都会根据元素的键值自动排序。Map所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值。
我们可以通过map的迭代器改变map的键值吗?答案是不行,因为map的键值关系到map元素的排列规则,任意改变map键值将会严重破坏map组织。如果想要修改元素的实值,那么是可以的。
Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。
Multimap和map的操作类似,唯一区别multimap键值可重复。
Map和multimap都是以红黑树为底层实现机制。
map/multimap常用操作
构造函数
1
2map<T1, T2> mapTT;//map默认构造函数:
map(const map &mp);//拷贝构造函数赋值操作
1
2map& operator=(const map &mp);//重载等号操作符
swap(mp);//交换两个集合容器大小操作
1
2size();//返回容器中元素的数目
empty();//判断容器是否为空插入操作
1
2
3
4
5
6
7
8
9
10
11map.insert(...); //往容器插入元素,返回pair<iterator,bool>
map<int, string> mapStu;
// 第一种 通过pair的方式插入对象
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过pair的方式插入对象
mapStu.inset(make_pair(-1, "校长"));
// 第三种 通过value_type的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值
mapStu[3] = "小刘";
mapStu[5] = "小王";删除元素
1
2
3
4clear();//删除所有元素
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg,end);//删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(keyElem);//删除容器中key为keyElem的对组。查找操作
1
2
3
4
5find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回map.end();
count(keyElem);//返回容器中key为keyElem的对组个数。对map来说,要么是0,要么是1。对multimap来说,值可能大于1。
lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器。
upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器。
equal_range(keyElem);//返回容器中key与keyElem相等的上下限的两个迭代器。
multimap案例
公司今天招聘了5个员工,5名员工进入公司之后,需要指派员工在那个部门工作
人员信息有: 姓名 年龄 电话 工资等组成
通过Multimap进行信息的插入 保存 显示
分部门显示员工信息 显示全部员工信息
1 |
|
算法
函数对象
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。
注意:
1.函数对象(仿函数)是一个类,不是一个函数。
2.函数对象(仿函数)重载了”() ”操作符使得它可以像函数一样调用。
分类:假定某个类有一个重载的operator(),而且重载的operator()要求获取一个参数,我们就将这个类称为“一元仿函数”(unary functor);相反,如果重载的operator()要求获取两个参数,就将这个类称为“二元仿函数”(binary functor)。
函数对象的作用主要是什么?STL提供的算法往往都有两个版本,其中一个版本表现出最常用的某种运算,另一版本则允许用户通过template参数的形式来指定所要采取的策略。
1 | //函数对象是重载了函数调用符号的类 |
谓语
谓词是指普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。
1 | class GreaterThenFive |
内建函数对象
STL内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。这些仿函数所产生的对象,用法和一般函数完全相同,当然我们还可以产生无名的临时对象来履行函数功能。使用内建函数对象,需要引入头文件 #include。
6个算数类函数对象,除了negate是一元运算,其他都是二元运算。
template<*class** T> T plus
template*<*class** T> T minus
template*<*class** T> T multiplies
*template*<*class** T> T divides
*template*<*class** T> T modulus
*template*<*class** T> T negate
6个关系运算类函数对象,每一种都是二元运算。
*template*<*class** T> bool equal_to
*template*<*class** T> bool not_equal_to
*template*<*class** T> bool greater
*template*<*class** T> bool greater_equal
*template*<*class** T> bool less
*template*<*class** T> bool less_equal
逻辑运算类运算函数,not为一元运算,其余为二元运算。
*template*<*class** T> bool logical_and
*template*<*class** T> bool logical_or
*template*<*class** T> bool logical_not
内建函数对象举例:
1 | //取反仿函数 |
函数对象适配器
1 | //函数适配器bind1st bind2nd |
算法概述
算法主要是由头文件组成。
是所有STL头文件中最大的一个,其中常用的功能涉及到比较,交换,查找,遍历,复制,修改,反转,排序,合并等…
体积很小,只包括在几个序列容器上进行的简单运算的模板函数.
定义了一些模板类,用以声明函数对象。
常用遍历算法
for_each遍历算法
1 | /* |
1 | /*template<class _InIt,class _Fn1> inline |
transform算法
1 | /* |
1 | //transform 将一个容器中的值搬运到另一个容器中 |
常用查找算法
1 | /* |
常用排序算法
1 | /* |
常用拷贝和替换算法
1 | /* |
常用算数生成算法
1 | /* |
常用集合算法
1 | /* |