类和对象总结
将一个程序中关于某个对象的一些属性以及对这些属性执行的操作封装整合起来生成一个结构就是类(类似于
struct
)
类和对象
类的声明
用到关键字 class
,并在后面依次包含类名,一组放在 {}
里的成员属性和成员函数,以及结尾的分号。
类的声明声明将类本身及其属性告诉编译器。类声明本身并不能改程序的行为。必须要使用类,就像需要调用函数一样。
1 | class Complex//关于复数类的定义 |
封装指的是将数据以及使用他们的函数进行逻辑编组。这是面向对象编程的重要特征。类成员的函数也经常用术语“方法”代替
类的成员函数的定义
一种是在类的内部对成员进行定义,一种是在外部进行定义。
格式:返还类型 类名::成员函数名(参数列表){函数体}
1 | void Complex::setReal(double r){ |
注意:类定义中的成员函数只是函数的声明,需要给出每一个成员函数的定义(函数体),定义的参数类型的先后顺序和名字必须一致。
::
被称为作用域解析运算符,例如Human::dataOfBirth
指的是 Human 类中声明的变量dataOfBirth
,而::dataOfBirth
表示全局作用域中的变量dataOfBirth
作为类实例的对象
类相当于蓝图。仅声明类并不会对程序的执行产生影响。在程序执行阶段,对象是类的化身,要使用类的功能通常需要创建其实例——对象,并通过对象访问成员方法和属性。
创建 Human
对象与创建其他数据类型的实例类似:
1 | double pi=3.1415926535; |
就像可以使用其他数据类型动态分配内存一样,也可以使用 new
为 Human
对象动态分配内存:
1 | int *pointsToNum=new int; |
类的成员的访问
- 用句点运算符
.
- 用指针运算符
->
1 | Human firstMan;//实例化 |
关键字public
,private
,protected
-
public
:public
表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用。 -
private
:private
表示私有,私有的意思就是除了class
自己成员之外,任何人都不可以直接使用。 -
protected
:protected
对于子女、朋友来说,就是public
的,可以自由使用,没有任何限制,而对于其他的外部class
,protected
就变成private
。
有一些保密属性不能被外界修改访问,用
protected
后就只能在类的内(或者友元)部进行访问
注意:默认情况下,所有的成员都是私有的
通过对象只能访问公有成员,而不能访问私有成员,保护私有内部成员,放置被意外破坏,达到封装与保护的效果。
一般成员函数在 public
中作为外界的接口,属性在 private
中只可被 class
内部的成员函数访问,这样就防止了属性被更改。
一个隐藏自身年龄的程序:
1 |
|
由于
age
是一个私有成员,因此只能通过class
函数的GetAge()
函数获取,但是该接口函数对真实年龄做了预处理实现了隐藏真实年龄。
构造函数
构造函数是类中一种特殊的函数,在根据类创建对象时被调用。与函数一样,构造函数也可以重载。
声明和实现构造函数
构造函数有以下特点:
- 在类定义时没有定义构造函数,编译系统会自动生成一个默认构造函数
- 函数名称和类的名称完全相同
- 函数没有返还类型,连
void
也没有 - 可以有参数或者没有参数
- 通常是放在
public
下 - 总是在创建对象的时候被调用,这让它成为将成员变量初始化为选定值的理想场所
构造函数的定义可在类声明中也可在类声明外
1 | class Human |
重载构造函数
默认值构造函数重载
一个完整的类通常会实现以下三种构造函数
- 无参数构造函数(默认构造函数)
注意:当重载构造函数之后就编译器不会再自动生成
默认构造函数是调用时可不提供参数的构造函数,并不一定是不接受任何参数的构造函数。因此,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class Complex
{
puiblic:
Complex();
private:
double real,imag;
};
Complex::Complex(){
real=0,imag=0;
}
int main()
{
Complex f0;//实例化时仍不需要传入任何参数
return 0;
}
- 带参数构造函数
为复数类提供单个重载构造函数
1 | class Complex |
问题:上述函数若是既想要创建不含参的,还要创建含某个参的或者是传入所有参,那怎么办呢,这时候需要多个版本的构造函数重载
为复数类提供多个重载构造函数
1 | class Complex |
多个版本的构造函数形成重载关系,到底调用哪个构造函数由用户创建对象时指定的参数进行匹配。
实质:构造函数的匹配实际上遵循函数重载的匹配规则
问题:虽然这样可以实现上述要求,但是若是多个参数可组成很多组合形式,一个个构造函数不切实际,这时需要另一种方法:带参数缺省值的构造函数重载
带参数缺省值的构造函数重载
规则:
1.声明成员函数时,可以指定缺省的值,以方便调用。为某个参数设置缺省值后,其右侧的参数都应该设置缺省值,否则编译器报错
2.缺省值只能在成员函数声明中出现,不能在成员函数定义时出现
1 | class Complex |
用户没有提供所有的参数时,由编译器补全再调用相应的函数
Complex f1(8,14);
——>Complex f1(8,14);
Complex f2;
——>Complex f2(0,0);
Complex f3;
——>Complex f3(5,0);
包含初始化列表的构造函数
构造函数对初始化成员变化很有用,另一种初始化成员的方式是使用初始化列表
规则:初始化列表由包含在括号中的参数声明中的后面的冒号标识,冒号后面列出了各个成员变量及其初始值,以逗号分隔初始化字段。初始值可以是参数,也可以是固定值。使用特定参数调用基类的构造函数时初始化列表很有用。
同样初始化列表可在声明内也可在声明外
构造函数名(参数列表){数据成员=参数;}
构造函数名(参数列表):初始化列表{}
无论是在构造函数初始化列表中初始数据成员,还是在构造函数体中对它们赋值,最终结果是相同的。当数据成员是对象时,两种方法的性能上会有很大差别。C++构造函数初始化列表与构造函数中的赋值的区别
注意:类中 const
常量,必须在初始化列表中初始化,不能使用构造函数赋值的方式初始化
1 | class Complex |
注意:初始化的时候顺序是按照类声明内的数据类型的声明顺序,而不是按照初始化列表的顺序,所以写初始化列表的时候对应类的声明内的数据类型声明顺序写是一个好习惯,可以优化性能
初始化列表和构造函数的赋值优先是初始化列表进行的
综合两种方法,在初始化对象的时候可以用带缺省值的构造函数并用初始化列表来初始化对象成员
复制(拷贝)构造函数
复制构造函数是一种特殊的构造函数,形参是本类对象的引用(常引用),用途就是用一个已经存在的同类型的对象去初始化当前的新的对象。
在调用函数的时候,实参被复制一份传给函数的形参,这种规则也适用于对象(类的实例)
1
2 double Area(double radius);
//调用的时候实参被复制传给radius
每个类都有一个复制构造函数,可以自定义,若是未定义复制构造函数,系统会自动生成带参数缺省值的构造函数,用于复制数据成员完全相同的对象。
例:没有定义系统自动生成的复数类的默认构造函数
1 | class Complex |
使用
const
防止传入的c
被意外的修改在构造函数中可以通过
c
对象访问私有成员的real
和image
注意:依次会为所有的数据成员赋值,但是当含有指针成员的时候容易出现问题
调用时机
当涉及要使用现有的对象初始化新对象的时候
1 | Complex f2=f1;//两种都可以初始化 |
调用函数会按值传递,函数返回对象也是按值传递
1 | Complex ff(const Complex c){ |
在调用
ff
函数的时候,调用复制构造函数赋值对象c1
的内容;ff
函数返回后调用析构函数并销毁c;函数返回时创建临时对象,调用复制构造函数复制c的内容;最后析构函数销毁临时对象;
注意:已经存在的对象之间使用 =
时不会调用构造复制函数,而是普通的赋值
1 | class Cmyclass |
减少使用方法
在调用赋值构造函数的时候会产生时间上的大量消费,而合理的减少使用它可以提高程序的效率
两种方法:指针和引用
本质上都是一样的,就是不产生新的对象就不会调用赋值构造函数
**
浅复制及其存在问题
上述也有提到默认的构造函数在处理指针赋值的时候会出现一些问题,这就是浅复制导致的。
1 | class String |
上述的类实例化的对象不再作用域内的时候会通过析构函数的
delete
销毁指针变量buffer
占用缓存区,但是在用a
初始化新的对象b
的时候,指针所指向的内存块不会再复制一份,所以b
内的指针也是指向a
的指针所指向的内存块(结果就是两个对象指向同一个内存块)。假若销毁两个对象其中的一个时候,其指针所指向的内存块将被释放掉,导致另一个对象存储的指针拷贝无效。所以在销毁剩下的一个对向时候由于指针的指向缓存区已经被delete
了,再次销毁就会可能导致程序稳定性被破坏
所以在调用复制构造函数的时候涉及指针变量的拷贝,需要自定义复制构造函数而不能用默认的
析构函数
与构造函数一样,析构函数也是一种特殊的函数。构造函数在实例化对象时被调用,而析构函数在对象被销毁的时候被调用。
声明和实现析构函数
析构函数有以下特点:
- 如果声明了构造函数,往往也需要声明析构函数,析构函数用于在对象消亡时执行清理工作并释放分配给对象的内存
- 析构函数名称和类名称前面加上一个~
- 析构函数同样没有返还类型
- 析构函数不能出传递任何参数
- 析构函数通常放在
public
下
同样构造函数的定义可在类声明中也可在类声明外
1 | class Human |
如果在类定义是没有定义析构函数,编译系统会自动生成一个默认析构函数。
1 | class Complex |
每当对象不再在作用域内或通过 delete
被删除进而被销毁时,都将通过调用析构函数,这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。通常情况下载构造函数中使用 new
分配内存空间,析构函数中需要用 delete
释放。
1 | class Complex |
在堆中使用对象
类似在堆中创建一个普通的数据空间,也可以在堆中创建一个类的对象。
同样的在堆中使用完创建的对象之后,需要通过 delete
释放相应的存储空间。
下面的程序中:一个是在栈(主函数)中使用对象,一个是程序员自主在堆中使用对象
由于是在堆中创建的对象,同样只能通过指针间接访问
-
指针解除引用,通过
.
运算符访问(*pComplex).setReal(3);
-
通过指针的指向运算符
->
直接访问,更加方便pComplex->setReal(3)
成员函数
成员函数是通过调用类的实例的内部函数,这个函数既可以访问自身 private
,还可以访问同类的其他实例的 private
1 |
|
常成员函数( const
)
定义:类中某些函数只会读取数据成员,而不会修改数据成员,作为一种良好的契约和习惯,应该将该成员函数声明为常成员函数
方法:在函数头的结尾加上 const
作用:在常成员函数内部,如果直接修改数据成员(或者是调用非常成员函数间接修改数据成员)编译器都会报错
常对象
- 声明为常量的对象,其数据成员不能被修改
- 通过常对象只能访问常成员函数,不能调用非常成员函数,但是可以调用公有的静态成员函数
- 常引用和指向常对象的指针也不能修改成对象的数据成员
注意:指向常对象的指针和指向对象的常指针是不一样的
判断下列哪些是正确的操作:
- 第一个是常成员函数:可以调用只读数据成员的函数
getValue()
,但是不可以调用修改数据成员的函数setValue()
- 第二个是指向常对象的指针,不可以调用修改数据成员函数
setValue()
修改对象的参数。但是指针指向的对象可以修改- 第三个是指向对象的常指针,由于对象不是常对象,所以指针可以修改指向对象的参数。但是指针本身不可以修改
常指针和指向常量的指针也是很好区分的:看 *
和 const
谁离指针变量名近,离的近的就是修饰指针变量的
1 | //以下两种为指向常量的指针 |
友元函数
友元是出于执行效率的考虑,允许类外部的函数或其他的类通过对象名直接访问本类的私有成员称为友元函数或友元类。
注意:友元打破了封装和数据隐藏,除非一些特殊场合,应该慎重考虑
规则:友元函数是类外部的函数,但是声明是必须在类的内部完成,需要在声明的函数前加上 friend
关键字。
友元函数的声明放在私有部分和公有部分的作用效果是一样的
1 |
|
若是代码中
add
不是友元函数(即普通函数),那么作为一个类的外部函数则不能直接访问private
了,所以只能通过getImag()
,getReal()
两个类的公有函数接口间接访问private
普通函数
声明和定义均在类的外部(说白了就是普通的函数),那么它就不能访问 private
,只能访问 public
成员
1 |
|
静态成员
static
关键字在 C 语言中常常用到。在程序中的任何一个函数内普通的静止变量默认是不会初始化的,而且这个变量存储于进程栈的空间,使用完毕就会释放。如果在其前面加上 static
关键字变量,那么这个变量就会被放入全局数据区分配内存并初始化为 0(相当于变成了全局变量),即使函数返还它的值也会保持不变。
同样,也可以用 static
关键字这一特性在面向对象中使用:
在写类和对象的作业是发现这个关键字的优势,通过一道题来说明+介绍
1 | class Student{ |
count()
函数是Student
类的内部成员,负责统计录入的学生个数,也就是相当于整个程序都是在用一个count
来计数(相当于独一无二的全局变量),若是直接当做普通的内部成员在默认构造函数中初始化为 0 ,那么类每实例化一个就会产生新的一个count
(初始化为 0 ),无法满足题目的要求
静态数据成员
在类内数据成员的声明前加上 static
关键字,该数据成员就是类内的静态数据成员。其特点如下:
- 静态数据成员存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中定义(在类的内部先声明)
- 静态数据成员是类的成员,不管内存是否存在对象或者多少个对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝
- 由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作
- 和普通数据成员一样,静态数据成员也遵从
public
,protected
,private
访问规则
公有静态成员:可以在成员函数中随意访问,也可以通过类名称访问
私有成员函数:不能通过类名访问,但是可以借助对象调用公有成员函数接见访问
- 静态数据成员的初始化格式:
<数据类型><类名>::<静态数据成员名>=<值>
- 类的静态数据成员有两种访问方式:
<类对象名>.<静态数据成员名>
或<类类型名>::<静态数据成员名>
同全局变量相比,使用静态数据成员有两个优势:
- 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性
- 可以实现信息隐藏。静态数据成员可以是
private
成员,而全局变量不能
1 |
|
静态成员函数
引入:多个对象”共享“同一个静态数据成员,可以随意访问,容易造成混乱。解决办法:将静态数据成员定义为 private
,通过公有的静态成员函数访问
与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象,其特性如下:
- 静态成员函数没有
this
指针,它无法访问属于类对象的非静态数据成员(普通函数都不行),也无法访问非静态成员函数,它只能调用其余的静态成员函数 - 出现在类体外的函数定义不能指定关键字
static
- 非静态成员函数可以任意地访问静态成员函数和静态数据成员
1 | class Test |
对象成员
-
在创建复杂类的时候,经常讲简单的类对象作为其成
-
简单类组合构成符合类,构成了存在某些关系的类
-
当一个类的对象作为另一个类的成员时,称该对象为对象成员,这种方法为“组合技术”
-
包含对象作为数据成员的类成为容器类,当创建容器类对象时,对象成员的初始化由构造函数的初始化列表提供
比如要是设计“计算一个关于三角形类的距离“程序,那么我可以先定义关于点的类(保存关于点的参数),然后在三角形类定义的时候调用点的类的头文件和计算距离函数。客户端的程序只用把包装好的上述的程序调用头文件。
了解即可
好处:如果由二维空间转向三维空间,客户端不是透明的用户只用关心Trangle类的接口而不关心其实现,只需要更改那几个头文件的参数。
运算符重载
引入:表达式 9/2=4
和 9.0/2.0=4.5
的运算符 /
具有不同的意义原因就是运算符重载
C++ 是由函数组成的,在C++
的内部,任何运算通过写好的函数实现的。在处理表达式 8+7
的时候,C++ 的表达式被解释为如下函数调用表达式
1 | operator + (8,7); |
operator()
让对象像函数,被称为函数运算符,用途主要是决策。根据使用的操作数数量,这样的函数通常称为单目谓词或者双目谓词。
1
2
3
4
5
6
7
8
9
10
11
12
13 class Display
{
void operator () (string) const{
cout<<input<<endl;
}
};
int mian()
{
Display displayFuncobj;
dispayFuncobj("Display this string!");
return 0;
}在第11行将对象
displayFuncobj
用作函数是因为编译器隐式的把它转换为了对函数operator()
的操作。
operator
将()
运算符重载
- 相同的运算符对于不同的数据类型有不同的操作,实质上就是函数的重载实现的
运算符重载时 C++ 的强大功能和特色,多数运算符都能够重载,但是有一些约束条件。
- 不允许重载内置类型的运算符
- 不能改变运算符的优先级和目数
- 不能创建新的运算符,如将
*
声明为指数运算符 - 改变运算符的语义是合法的,但是重载
+
运算符作减法运算会导致代码难以理解。重载运算符的初衷就是便于使用和理解
可/不可重载运算符
下面是可重载的运算符列表:
类别 符号 双目算术运算符 +
(加),-
(减),*
(乘),/
(除),%
(取模)关系运算符 ==
(等于),!=
(不等于),<
(小于),>
(大于),<=
(小于等于),>=
(大于等于)逻辑运算符 ||
(逻辑或),&&
(逻辑与),!
(逻辑非)单目运算符 +
(正),-
(负),*
(指针),&
(取地址)自增自减运算符 ++
(自增),--
(自减)位运算符 |
(按位或),&
(按位与),~
(按位取反),^
(按位异或),<<
(左移),>>
(右移)赋值运算符 =
,+=
,-=
,*=
,/=
,% =
,&=
,|=
,^=
,<<=
,>>=
空间申请与释放 new
,delete
,new[]
,delete[]
其他运算符 ()
(函数调用),->
(成员访问),,
(逗号),[]
(下标)下面是不可重载的运算符列表:
.
:成员访问运算符.
,->
:成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符- **
#
: **预处理符号
加法运算符重载 +
1 | class Complex |
a+b
相当于a.operator+(b)
,返还临时复制对象并将临时复制对象赋值给c
复合运算符重载 +=
1 | class Complex |
a+=b
相当于a.operator+=(b)
,返还a
的引用
赋值运算符=
- 相同类型之间赋值时实际上也是通过调用
operator=
冲原函数实现的。如果类中没有定义该函数,编译器会提供默认的赋值运算符函数,实现对象数据成员之间的逐一赋值。 - 如果类中只包含简单的数据成员,默认的赋值运算符足以完成
1 | class Complex |
对于简单的类(不含有指针成员),无需重载
operator=
运算符,利用缺省的赋值功能也能够正常工作。
- 重载为类的成员函数
arguement-list
中参数的个数比原operator_symble
所需要的参数个数少一个(后置++
、--
除外) - 重载了类的友元函数
arguement-list
中参数的个数与原operator_symble
所需要的的参数个数相同
输入/输出运算符 >>
/ <<
1 | class Counter{ |
operator<<
必须重载为友元函数,参数为输入流对象和被输出对象,返还输出流对象,以支持连续输出operator<<
运算符函数将c
的内容输出到传入的输出流,并返还out
的引用- 支持形同
cout<<c1<<c2
的级联式输出
混合运算
STL模板库
是一组模板类和函数供程序员调用
- 用于存储信息的容器
- 用于访问容器存储的信息的迭代器
- 用于操作容器内容的算法
STL容器分类
分类:顺序容器、关联容器和特殊的变种容器适配器(包含的功能有限,用于满足特殊的要求)
顺序容器
- 按照顺序存储数据,如数组和列表
- 插入速度快但是查找相对较慢
容器 | 特点 |
---|---|
std::vector |
动态数组,在最后插入或者删除元素,类似书架 |
std::deque |
与 vector 类似,但是允许在开头插入或者删除元素 |
std::list |
双向链表,在任意位置添加或者删除元素 |
std::forward_list |
单向链表,只能沿一个方向遍历 |
std::vector
与数组类似,可以用[]来随机访问元素,可以根据应用程序在运行阶段的需求自动调整长度,为了保留数组能够根据位置随机访问元素,std::vector
实现都将所有的元素存储在连续的存储单元内,这也会降低程序的性能
std::list
不能像vector
那样随机访问,但是可以使用不连续的内存块来存储元素,所以不需要重新分配内存块导致性能问题
关联容器
- 按照指定顺序存储元素,就像字典一样
- 插入速度较慢但是查询速度很快
容器 | 特点 | 复杂度 |
---|---|---|
std::set |
存储不同的值(去重),插入时进行排序 | 对数 |
std::unordered_set |
存储不同的值(去重),插入时进行排序 | 常数 |
std::map |
存储键—值对,根据唯一的存储键排序 | 对数 |
std::unordered_map |
存储键—值对,根据唯一的存储键排序 | 对数 |
std::multiset |
与 set 类似,但是不去重 |
|
std::unordered_multiset |
与 std::multiset 类似,不去重 |
|
std::multimap |
与 map 类似,但不去重 |
|
std::unordered_multimap |
与 underordered_map 类似,不去重 |
容器适配器
- 关联容器和顺序容器的变种,功能有限,用于满足特殊要求
容器 | 特点 |
---|---|
std::stack |
LIIFO (先进后出)方式存储元素,在栈顶压入和弹出元素 |
std::queue |
FIFO (先进先出)方式存储元素,删除最先插入的元素 |
std::priority_queue |
以特定的顺序存储元素,优先级最高的元素总是位于队列开头 |
STL迭代器概述
最简单的迭代器是指针,给定一个指向数组中的第一个元素的指针,那么可递增指针使其移向下一个元素,还可以直接对当前位置的指针进行操作
STL的迭代器是模板类,泛型指针。它可以对容器或者以模板函数的方式提供的 STL 算法进行操作,迭代器是一座桥梁,让这些模板函数以一种无缝连接的方式处理容器
STL算法概述
下列算法都是 std
命名空间中的模板函数,要使用他们必须包含标准头文件 <algorithm>
算法 | 功能 |
---|---|
std::find |
在集合中查找元素 |
std::find_if |
根据用户指定的谓词在集合中查找元素 |
std::reverse |
反转集合中元素的排列顺序 |
std::remove_if |
根据用户定义的谓词将元素从集合中删除 |
std::transform |
使用用户定义的变换函数对容器中的元素进行变换 |
STL string
类
std::string
是在标准库 (注意不是 C 语言中的 库)中提供的一个类,本质上是 std::basic_string
的别称。
特点:
- 使用简单,相较于其他STL容器,
string
的常数非常优秀,与字符串不相上下 - 可以动态分配内存,可以直接用
cin
来输入,但是速度相对较慢 - 重载了加法运算符和比较运算符
- 同样是按照字典序排序,可以用
sort()
函数
知识补充
声明定义都会就不写了,主要记录几个不太会的
1 | //输入 |
puts
,gets
是以指针形式存入,只是用于字符数组,可以接受空格、Tab
常用成员函数
1 | //查找字符和子字符串 |
常用的函数
1 | //反转函数 |
STL动态数组类
动态数组让程序员能够灵活的存储数据,无需在编写程序的时候就知道数组的长度
std::vector
特点:提供了动态数组的通用功能
- 在数组末尾添加元素的时间是固定的,不随数组大小而异,删除亦然
- 在数组的中间添加或者删除元素所需的时间是与该元素后面的元素个数成正比
- 存储的元素是动态的,
vector
负责管理内存 - 头文件
#include<vector>
访问
-
at()
v.at(pos)
返回容器中下标为pos
的引用。如果数组越界抛出std::out_of_range
类型的异常。 -
operator[]
v[pos]
返回容器中下标为pos
的引用。不执行越界检查。 -
front()
v.front()
返回首元素的引用。 -
back()
v.back()
返回末尾元素的引用。 -
data()
v.data()
返回指向数组第一个元素的指针。
迭代器
-
begin()/cbegin()
返回指向首元素的迭代器,其中
*begin = front
-
end()/cend()
返回指向数组尾端占位符的迭代器,注意是没有元素的。
-
rbegin()/rcbegin()
返回指向逆向数组的首元素的逆向迭代器,可以理解为正向容器的末元素。
-
rend()/rcend()
返回指向逆向数组末元素后一位置的迭代器,对应容器首的前一个位置,没有元素。
以上列出的迭代器中,含有字符 c
的为只读迭代器,你不能通过只读迭代器去修改 vector
中的元素的值。如果一个 vector
本身就是只读的,那么它的一般迭代器和只读迭代器完全等价。只读迭代器自 C++11 开始支持。
长度和容量
vector
有以下几个与容器长度和容量相关的函数。注意, vector
的长度( size
)指有效元素数量,而容量( capacity
)指其实际分配的内存长度,相关细节请参见后文的实现细节介绍。
与长度相关:
empty()
返回一个bool
值,即v.begin() == v.end()
,true
为空,false
为非空。size()
返回容器长度(元素数量),即std::distance(v.begin(), v.end())
resize()
改变vector
的长度,多退少补。补充元素可以由参数指定max_size()
返回容器的最大可能长度
与容量相关:
reserve()
使得vector
预留一定的内存空间,避免不必要的内存拷贝capacity()
返回容器的容量,即不发生拷贝的情况下容器的长度上限shrink_to_fit()
使得vector
的容量与长度一致,多退但不会少补
vector
的长度(大小)实际上指的是实际存储的元素数量,而vector
容量指的是在重新分配内存以存储更多的元素前vector
能够存储的元素数量。因此,vector
长度 <= 容量
初始化
vector
便利的初始化
由于 vector
重载了 =
运算符,所以我们可以方便的初始化。此外从 C++11 起 vector
还支持列表初始化,例如 vector data{1,2,3};
1 | //定义具有10个整型元素的向量(尖括号为元素类型名,它可以是任何合法的数据类型),不具有初值,其值不确定 |
常用成员函数
1 | //清空a中的元素 |
vector
重写了比较运算符及赋值运算符
vector
重载了六个比较运算符,以字典序实现,这使得我们可以方便的判断两个容器是否相等(复杂度与容器大小成线性关系)。例如可以利用 vector
实现字符串比较(当然,还是用 std::string
会更快更方便)。另外 vector
也重载了赋值运算符,使得数组拷贝更加方便。
添加元素的方法
1.向向量a中添加元素
1 | vector<int>a; |
2.从数组中选择元素向向量中添加
1 | int a[6]={1,2,3,4,5,6}; |
3.从现有向量中选择元素向向量中添加
1 | int a[6]={1,2,3,4,5,6}; |
4.从文件中读取元素向向量中添加
1 | ifstream in("data.txt"); |
常见错误赋值方式
1 | vector<int>a; |
从向量中读取元素
1.通过下标方式获取
1 | int a[6]={1,2,3,4,5,6}; |
2.通过迭代器方式读取
1 | int a[6]={1,2,3,4,5,6}; |
常用的函数
1 |
|
std::deque
和 vector
的不同就在于支持在数组开头和末尾插入或者删除元素,需要包含头文件#include<deque>
补充
vector
的内置函数std::deque
同样适用,而且还可以用push_front()
和pop_front()
在开头和结尾插入删除元素
1 | //在a的开头向量前插入一个元素,其值为5 |
STL链表
std::list
std::list
是 STL 提供的双向链表数据结构。能够提供线性复杂度的随机访问,以及常数复杂度的插入和删除。
特点:
- 链表是由一系列节点组成,其中每个节点除包含对象或者值外还指向下一个节点的位置,双向链表还指向前一个节点的位置。
- 链表允许从开头、中间和结尾以固定的时间插入元素
std
命名空间中的模板list
是一种泛型实现,要使其成员函数需要实例化模板- 动态存储不存在浪费空间和溢出
- 头文件
include<list>
迭代器
和
std::vector
一样
初始化
1 | list<int> c0; //空链表 |
常用成员函数
list
也可以像 std::vector
一样用 =
,>
,<
等运算符,好像其他容器也应该是被重载了容器,有待考究。
1 | c.assign(n,num);//将n个num拷贝给链表c |
注意:sort()
函数是不能给 list
排序的,因为 list
迭代器好像不一样( random类型
),要用 list
自己的 sort()
成员函数
std::forward_list
C++11 引入了 std::forward_list
,它是单向链表,只允许沿着一个方向遍历
需要包含 include<forward_list>
头文件
特点:
- 头文件:
#include<fstream>
- 采用的是头插法,只能用
push_front()
说明:因为单向链表只用记录后一个节点的位置了,所以相对于双向链表来说节省了很多的空间
STL集合类
STL
向程序员提供了一些容器类,一遍在应用程序内进行频繁而快速的搜索。std::set
和 std::multiset
用于存储一组经过排序的元素,其查找元素的复杂度为对数,而 unordered
集合的插入和查找的时间是固定的。
简介
容器 set
和 multiset
让程序员能够在容器中快速查找键,键是存储在一维容器中的值。set
和 multiset
之间的区别在于后者存储重复的值,而前者只能存储唯一的值。
为了快速实现搜索,他们的内部结构都是类似二叉树的红黑树。在将元素插入容器的时候是进行排序的,从而提高的查找速度。这也就意味着:
set
不能像vector
那样可以使用其他的元素替换给定位置的值set
存储式去重比较后再插入- 必须使用头文件
#include<set>
下面以
set
容器讲解
初始化
1 | set<int> seta; //默认是小于比较器less<int>的set |
常用成员函数
1 | //插入 |
在需要频繁查找的应用程序中(多次用到 find()
操作),set
和 multiset
很有优势,因为在插入的时候已经排好顺序,查找速度很快。但是因此也导致插入时需要花费额外的时间
STL散列集合
相交于未经排序的容器(查找时间和元素数成正比),排好序的容器极大地改善了性能。前辈不满于现状,通过努力还把集合类容器的插入和排序时间变为了固定值。std::unordered_set
和 std::unordered_multiset
是基于散列实现的容器,即使用散列函数来计算索引。将元素插入散列集合时,首先使用散列函数计算出唯一的索引,然后在根据索引决定将元素放入哪一个桶中
- 需要使用头文件
#include<unordered_set>
使用上还有点区别,现在看不懂会在在写吧
使用 std::fstream
处理文件
C++ 提供 std::fstream
旨在以独立平台的方式访问文件。std::fstream
从 std::ofstream
那里继承了写入文件的功能,并从 std::ifstream
那里继承了读入文件的功能。
所有的操作对象都是针对于内存的,
std::ifstream
是从文件传入内存,std::oftream
是从内存传出文件
使用 open()
和 close()
打开关闭文件
在 fstream
类中,有一个成员函数 open()
,就是用来打开文件的,其原型是:
1 | fstream myFile; |
一般路径要提供从所在根目录开始的路径,若是没有路径系统默认是当前目录设置。
使用 open()
后都要先判断文件是否成功打开,只有打开成功了才使用文件流对象,一定要记得结束执行程序前关闭文件和内存的联系保护文件
1 | if(myFile)//成功打开返还非0值 |
打开文件的方式在类 ios
(是所有流式I/O
类的基类)中定义,常用的值如下:
打开方式 | 作用 |
---|---|
ios::app |
以追加的方式打开文件 |
ios::ate |
文件打开后定位到文件尾,ios:ap 就包含有此属性 |
ios::binary |
以二进制方式打开文件,缺省的方式是文本方式 |
ios::in |
文件以输入方式打开(文件数据输入到内存) |
ios::out |
文件以输出方式打开(内存数据输出到文件) |
ios::nocreate |
不建立文件,所以文件不存在时打开失败 |
ios::noreplac |
不覆盖文件,所以打开文件时如果文件存在失败 |
ios::trunc |
如果文件存在,把文件长度设为 0 ,也就是重建文件内容 |
可以把以上属性连接起来,如
ios::out|ios::binary
关于 binary 文件和默认文本文件: binary 文件读取快但存取范围小,不声明系统默认为文本文件,按照字节存取;文本文件读取慢(因为需要转换为 binary 文件)但是存取范围大,按照 ACSII 值存取
文件的读写
1.文本文件的读写
文本文件的读写很简单:用插入器 <<
向文件输出;用析取器 >>
从文件输入。假设 input
是以输入方式打开,output
以输出打开。示例如下:
1 | output << "I Love You";//向文件输出字符串"I Love You" |
这种方式还有一种简单的格式化能力,比如可以指定输出为 16 进制等等,具体的格式有以下一些
操纵符 | 功能 | 输入/输出 |
---|---|---|
dec |
格式化为十进制数值数据 | 输入和输出 |
endl |
输出一个换行符并刷新此流 | 输出 |
ends |
输出一个空字符 | 输出 |
hex |
格式化为十六进制数值数据 | 输入和输出 |
oct |
格式化为八进制数值数据 | 输入和输出 |
setpxecision(int p) |
设置浮点数的精度位数 | 输出 |
比如要把 123 当作十六进制输出:
output<<hex<<123;
要把 3.1415926 以 5 位精度输出:
output<<setpxecision(5)<<3.1415926;
2. binary 文件的读写
(1) put()
put()
函数向流写入一个字符,其原型是 ofstream &put(char ch)
,使用也比较简单,如 output.put('c');
就是向流写一个字符 c
(2) get()
get()
函数比较灵活,有 3 种常用的重载形式:
一种就是和 put()
对应的形式:ifstream &get(char &ch);
功能是从流中读取一个字符,结果保存在引用ch
中,如果到文件尾,返回空字符。如 input.get(x);
表示从文件中读取一个字符,并把读取的字符保存在 x
中。
另一种重载形式的原型是: int get();
这种形式是从流中返回一个字符,如果到达文件尾,返回 EOF
,如x=input.get();
和上例功能是一样的。
还有一种形式的原型是:ifstream &get(char *buf,int num,char delim=’\n’);
这种形式把字符读入由 buf
指向的数组,直到读入了 num
个字符或遇到了由 delim
指定的字符,如果没使用 delim
这个参数,将使用缺省值换行符 \n
。例如:
input.get(str1,127,’A');
//从文件中读取字符到字符串 str1
,当遇到字符 A
或读取了 127 个字符时终止。
(3) 读写数据块
读写 binary 文件通常是使用 ofstream::write()
和ifstream::read()
。他们的原型如下:
read(unsigned char *buf,int num);write(const unsigned char *buf,int num);
read()
从文件中读取 num
个字符到 buf
指向的缓存中,如果在还未读入 num
字符时就到了文件尾,可以用成员函数 int gcount()
;来取得实际读取的字符数;而 write()
从buf 指向的缓存写num
个字符到文件中,值得注意的是缓存的类型是 unsigned char *
,有时可能需要类型转换。
1 | typedef struct _Student{ |
3.检测 EOF
这里真的很重要,要理解的是文件读入的时候只有读到终止符(不能输出的一个值)的时候,才会停止
eof()
检查文件是否到达终止符,是的话就返还 1 ,否为返还 0
错误样例
1 | while(!fin.eof()){ |
错因:当
fin.get()
读到了终止符的时候,还是输出了,但是因为终止符是一个不能输出的值,所以程序会再次输出文件的最后一个可输出的值
正确方法
1 | fin.get(ch); |
1 | while(!fin.eof()){ |
stringstream
转换数据类型/切割字符串
string
字符串
写着部分主要是为了整体上了解
string
和char[]
的不同之处
string
是 C++ 提供的字串型態,和 C 的字符串相比,除了有不限长度的优点外,还有其他许多方便的功能。
首先要取得其中某一个字元,和传统 C 的字串一样是用 s[i]
的方式取得。比较不一样的是如果 s
有三个字元,传统 C
的字串的 s[3]
是 0 字元,但是 C++ 的 string
则是只到 s[2] 这个字元而已
操作 | string |
字元阵列 |
---|---|---|
宣告字串 | string s; |
char s[100]; |
取得第 i 个字元 | s[i] |
s[i] |
字串长度 | s.length() 或s.size() |
strlen(s) |
读取一行 | getline(cin,s); |
gets(s); |
设成某字串 | s="TCGS"; |
strcpy(s,"TCGS"); |
字串相加 | s=s+"TCGS"; |
strcat(s,"TCGS"); |
字串比较 | s=="TCGS" |
strcmp(s,"TCGS") |
从上面的表格,我们可以发现
string
的用法比较直观,因此如果沒有特別的需要,尽量使用string
会比较方便。
stringstream
类
stringstream
是 C++ 提供的另一个字串型的串流( stream
)物件,和之前学过的 iostream
、fstream
有类似的操作方式。要使用 stringstream
, 必須先加入這一行:#include <sstream>
<sstream>
定义了三个类:istringstream
、ostringstream
和 stringstream
,分别用来进行流的输入、输出和输入输出操作。本文以 stringstream
为主,介绍流的输入和输出操作。
<sstream>
主要用来进行数据类型转换,由于 <sstream>
使用 string
对象来代替字符数组( snprintf
方式),就避免缓冲区溢出的危险;而且,因为传入参数和目标对象的类型会被自动推导出来,所以不存在错误的格式化符的问题。简单说,相比 C 库的数据类型转换而言,<sstream>
更加安全、自动和直接。
使用 stringstream
对象简化类型转换
数据类型转换
示例展示的是把
int
类型转换为string
类型
1 |
|
可以看到和
fstream
还是有一定区别的,stringstream
缓存区放在左侧,其他类型放在右侧,传入用<<
,传出用>>
相比传统 C 的转换
stringstream
的使用更加安全遍历,不用可以考虑内存的问题
多个字符拼接
本示例介绍在
stringstream
中存放多个字符串,实现多个字符串拼接的目的(其实完全可以使用string
类实现),同时,介绍stringstream
的清空方法。
1 |
|
- 可以使用
str()
方法,将stringstream
类型转换为string
类型; - 可以将多个字符串放入
stringstream
中,实现字符串的拼接目的; - 如果想清空
stringstream
,必须使用sstream.str("");
方式;clear()
方法适用于进行多次数据类型转换的场景。
stringstream
的清空
清空
stringstream
有两种方法:clear()
方法以及str("")
方法,这两种方法有不同的使用场景。str("")
方法的使用场景,在上面的示例中已经介绍了,这里介绍clear()
方法的使用场景。
1 |
|
在本示例涉及的场景下(多次数据类型转换),必须使用 clear()
方法清空 stringstream
,不使用 clear() 方法或使用 str("")
方法,都不能得到数据类型转换的正确结果
使用 stringstream
对象切割字符
stringstream
的另一个妙处就是按空格分隔字符串
1 |
|