类和对象总结

将一个程序中关于某个对象的一些属性以及对这些属性执行的操作封装整合起来生成一个结构就是类(类似于 struct )

类和对象

类的声明

用到关键字 class,并在后面依次包含类名,一组放在 {} 里的成员属性和成员函数,以及结尾的分号。

类的声明声明将类本身及其属性告诉编译器。类声明本身并不能改程序的行为。必须要使用类,就像需要调用函数一样。

1
2
3
4
5
6
7
8
9
10
11
class Complex//关于复数类的定义 
{
public://公有
void setReal(double r);
void setImag(double i);
double getReal();
double getImag();
void output();
private://私有
double real,imag;
}

封装指的是将数据以及使用他们的函数进行逻辑编组。这是面向对象编程的重要特征。类成员的函数也经常用术语“方法”代替

类的成员函数的定义

一种是在类的内部对成员进行定义,一种是在外部进行定义。

格式:返还类型 类名::成员函数名(参数列表){函数体}

1
2
3
4
5
6
7
8
9
10
11
12
13
void  Complex::setReal(double r){
real=r;
}

double Complex::getReal(){
return real;
}

void Complex::output(){
cout<<real;
if(imag>=0)cout<<"+";
cout<<imag<<"i"<<endl;
}

注意:类定义中的成员函数只是函数的声明,需要给出每一个成员函数的定义(函数体),定义的参数类型的先后顺序和名字必须一致。

:: 被称为作用域解析运算符,例如 Human::dataOfBirth 指的是 Human 类中声明的变量 dataOfBirth,而 ::dataOfBirth 表示全局作用域中的变量 dataOfBirth

作为类实例的对象

类相当于蓝图仅声明类并不会对程序的执行产生影响。在程序执行阶段,对象是类的化身,要使用类的功能通常需要创建其实例——对象,并通过对象访问成员方法和属性。

创建 Human 对象与创建其他数据类型的实例类似:

1
2
double pi=3.1415926535;
Human firstMan;

就像可以使用其他数据类型动态分配内存一样,也可以使用 newHuman 对象动态分配内存:

1
2
3
4
5
int *pointsToNum=new int;
delete pointsToNum;

Human* firstMan=new Human();
delete firstMan;

类的成员的访问

  • 用句点运算符 .
  • 用指针运算符 ->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Human firstMan;//实例化
Human *theMan=&firstMan;//建一个指针指向实例

//对象可以用两种运算符
//指针只能用->,但是当用间接运算符(*)获取对象后则也可以用句点运算符

firstMan.dataOfBirth=1970;
firstMan->dataOfBirth=1970;
theMan->dataOfBirth=1970;
(*the man).dataOfBirth=1970;

firstMan.introduceSelf();
firstMan->introduceSelf();
theman->introduceSelf();
(*theMan).introduceSelf();

delete theMan;

关键字public,private,protected

  • publicpublic 表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用。

  • privateprivate 表示私有,私有的意思就是除了 class 自己成员之外,任何人都不可以直接使用。

  • protectedprotected对于子女、朋友来说,就是 public 的,可以自由使用,没有任何限制,而对于其他的外部classprotected 就变成 private

有一些保密属性不能被外界修改访问,用 protected 后就只能在类的内(或者友元)部进行访问

注意:默认情况下,所有的成员都是私有的

通过对象只能访问公有成员,而不能访问私有成员,保护私有内部成员,放置被意外破坏,达到封装与保护的效果。

一般成员函数在 public 中作为外界的接口,属性在 private 中只可被 class 内部的成员函数访问,这样就防止了属性被更改。

一个隐藏自身年龄的程序:

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
#include<bits/stdc++.h>
using namespace std;

class Human
{
private:int age;
public:
void SetAge(int inputAge){
age=inputAge;
}

int GetAge(){
if(age>30)return age-2;
else return age;
}
};

int main()
{
Human firstMan;
firstMan.SetAge(35);

Human firstWoman;
firstWoman.SetAge(22);

cout<<"Age of firstMan"<<firstMan.GetAge()<<endl;
cout<<"Age of firstWoman"<<firstWoman.GetAge()<<endl;
return 0;
}

由于 age 是一个私有成员,因此只能通过 class 函数的 GetAge() 函数获取,但是该接口函数对真实年龄做了预处理实现了隐藏真实年龄。

构造函数

构造函数是类中一种特殊的函数,在根据类创建对象时被调用。与函数一样,构造函数也可以重载。

声明和实现构造函数

构造函数有以下特点:

  • 在类定义时没有定义构造函数,编译系统会自动生成一个默认构造函数
  • 函数名称和类的名称完全相同
  • 函数没有返还类型,连 void 也没有
  • 可以有参数或者没有参数
  • 通常是放在 public
  • 总是在创建对象的时候被调用,这让它成为将成员变量初始化为选定值的理想场所

构造函数的定义可在类声明中也可在类声明外

1
2
3
4
5
6
7
8
9
10
11
class Human
{
public:
Human();
};

Human::Human(){
//定义...
age=1;//初始化为1并输出一句话
cout<<"default"<<endl;
}

重载构造函数

默认值构造函数重载

一个完整的类通常会实现以下三种构造函数

  • 无参数构造函数(默认构造函数)

注意:当重载构造函数之后就编译器不会再自动生成

默认构造函数是调用时可不提供参数的构造函数,并不一定是不接受任何参数的构造函数。因此,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Complex
{
public:
Complex(double r,double i);
private:
double real,imag;
};

Complex::Complex(double r,double i){
real=r,imag=i;
}

int main()
{
Complex f1(8,14);
/*f2声明错误的,因为重载了带参数的构造函数
函数不在自动生称不含参的构造函数*/
Complex f2;
return 0;
}

问题:上述函数若是既想要创建不含参的,还要创建含某个参的或者是传入所有参,那怎么办呢,这时候需要多个版本的构造函数重载

为复数类提供多个重载构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Complex
{
public:
Complex(double r,double i){
real=r,imag=i;
};
Complex(double r){
real=r,imag=0;
}
Complex(){
real=0,imag=0;
}
private:
double real,imag;
};

int main()
{
Complex f1(8,14);
Complex f2;
Complex f3(5);
return 0;
}

多个版本的构造函数形成重载关系,到底调用哪个构造函数由用户创建对象时指定的参数进行匹配。

实质:构造函数的匹配实际上遵循函数重载的匹配规则

问题:虽然这样可以实现上述要求,但是若是多个参数可组成很多组合形式,一个个构造函数不切实际,这时需要另一种方法:带参数缺省值的构造函数重载

带参数缺省值的构造函数重载

规则:

1.声明成员函数时,可以指定缺省的值,以方便调用。为某个参数设置缺省值后,其右侧的参数都应该设置缺省值,否则编译器报错

2.缺省值只能在成员函数声明中出现,不能在成员函数定义时出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Complex
{
public:
Complex(double r=0,double i=0);
private:
double real,imag;
};

Comlex::Complex(double r,double i){//定义中不出现
real=r,imag=i;
}

int main()
{
Complex f1(8,14);
Complex f2;
Complex f3(5);
return 0;
}

用户没有提供所有的参数时,由编译器补全再调用相应的函数

Complex f1(8,14); ——> Complex f1(8,14);

Complex f2; ——> Complex f2(0,0);

Complex f3;——> Complex f3(5,0);

包含初始化列表的构造函数

构造函数对初始化成员变化很有用,另一种初始化成员的方式是使用初始化列表

规则:初始化列表由包含在括号中的参数声明中的后面的冒号标识,冒号后面列出了各个成员变量及其初始值,以逗号分隔初始化字段。初始值可以是参数,也可以是固定值。使用特定参数调用基类的构造函数时初始化列表很有用。

同样初始化列表可在声明内也可在声明外

构造函数名(参数列表){数据成员=参数;}

构造函数名(参数列表):初始化列表{}

无论是在构造函数初始化列表中初始数据成员,还是在构造函数体中对它们赋值,最终结果是相同的。当数据成员是对象时,两种方法的性能上会有很大差别。C++构造函数初始化列表与构造函数中的赋值的区别

注意:类中 const 常量,必须在初始化列表中初始化,不能使用构造函数赋值的方式初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
class Complex
{
public:
Complex(double r,double i);
private:
double real,double imag;
};

Complex::Complex(double r,double i)
:real(r),imag(i)
{
cout<<"default"<<endl;//其他的操作放大括号里
}

注意:初始化的时候顺序是按照类声明内的数据类型的声明顺序,而不是按照初始化列表的顺序,所以写初始化列表的时候对应类的声明内的数据类型声明顺序写是一个好习惯,可以优化性能

初始化列表和构造函数的赋值优先是初始化列表进行的

综合两种方法,在初始化对象的时候可以用带缺省值的构造函数并用初始化列表来初始化对象成员

复制(拷贝)构造函数

复制构造函数是一种特殊的构造函数,形参是本类对象的引用(常引用),用途就是用一个已经存在的同类型的对象去初始化当前的新的对象

在调用函数的时候,实参被复制一份传给函数的形参,这种规则也适用于对象(类的实例)

1
2
double Area(double radius);
//调用的时候实参被复制传给radius

每个类都有一个复制构造函数,可以自定义,若是未定义复制构造函数,系统会自动生成带参数缺省值的构造函数,用于复制数据成员完全相同的对象。

例:没有定义系统自动生成的复数类的默认构造函数

1
2
3
4
5
6
7
8
9
class Complex
{
public:
Complex(const Complex &c){
real=c.real;
image=c.image;
}
//...
};
  • 使用 const 防止传入的 c 被意外的修改

  • 在构造函数中可以通过 c 对象访问私有成员的 realimage

注意:依次会为所有的数据成员赋值,但是当含有指针成员的时候容易出现问题

调用时机

当涉及要使用现有的对象初始化新对象的时候

1
2
Complex f2=f1;//两种都可以初始化
Complex f2(f1);

调用函数会按值传递,函数返回对象也是按值传递

1
2
3
4
5
6
7
8
9
10
Complex ff(const Complex c){
return c;
}

int main()
{
Complex c1;
ff(c1);
return 0;
}

在调用 ff 函数的时候,调用复制构造函数赋值对象 c1 的内容;ff 函数返回后调用析构函数并销毁c;

函数返回时创建临时对象,调用复制构造函数复制c的内容;最后析构函数销毁临时对象;

注意:已经存在的对象之间使用 = 时不会调用构造复制函数,而是普通的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cmyclass
{
int n;
Cmyclass(){};
Cmyclass(Cmyclass &c){
n=2*c.n;
}
};

Cmyclass c1,c2;
c1.n=5;
c2=c1;
Cmyclass c3(c1);
//c2.n=5 c3.n=10

减少使用方法

在调用赋值构造函数的时候会产生时间上的大量消费,而合理的减少使用它可以提高程序的效率

两种方法:指针和引用

本质上都是一样的,就是不产生新的对象就不会调用赋值构造函数

**

浅复制及其存在问题

上述也有提到默认的构造函数在处理指针赋值的时候会出现一些问题,这就是浅复制导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class String
{
char *buffer;
//...
~String(){
delete [] buffer;
}
}

int main()
{
String a;
String b(a);//问题出现
return 0;
}

上述的类实例化的对象不再作用域内的时候会通过析构函数的 delete 销毁指针变量 buffer 占用缓存区,但是在用 a 初始化新的对象 b 的时候,指针所指向的内存块不会再复制一份,所以 b 内的指针也是指向 a 的指针所指向的内存块(结果就是两个对象指向同一个内存块)。假若销毁两个对象其中的一个时候,其指针所指向的内存块将被释放掉,导致另一个对象存储的指针拷贝无效。所以在销毁剩下的一个对向时候由于指针的指向缓存区已经被 delete 了,再次销毁就会可能导致程序稳定性被破坏

所以在调用复制构造函数的时候涉及指针变量的拷贝,需要自定义复制构造函数而不能用默认的

析构函数

与构造函数一样,析构函数也是一种特殊的函数。构造函数在实例化对象时被调用,而析构函数在对象被销毁的时候被调用。

声明和实现析构函数

析构函数有以下特点:

  • 如果声明了构造函数,往往也需要声明析构函数,析构函数用于在对象消亡时执行清理工作并释放分配给对象的内存
  • 析构函数名称和类名称前面加上一个~
  • 析构函数同样没有返还类型
  • 析构函数不能出传递任何参数
  • 析构函数通常放在 public

同样构造函数的定义可在类声明中也可在类声明外

1
2
3
4
5
6
7
8
9
class Human
{
public:
~Human()
};

Human::~Human(){
//销毁程序
}

如果在类定义是没有定义析构函数,编译系统会自动生成一个默认析构函数。

1
2
3
4
5
6
7
8
class Complex
{
public:
Complex(double r,double i);
~Complex(){}//编译系统提供的没有实质功能的默认析构函数
private:
double real,imag;
};

每当对象不再在作用域内或通过 delete 被删除进而被销毁时,都将通过调用析构函数,这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。通常情况下载构造函数中使用 new 分配内存空间,析构函数中需要用 delete 释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Complex
{
public:
Complex(double r,double i);
~Complex(){}
private:
double real,imag;
};

Complex::Complex(double r,double i){
double *p=r;
...//其他操作
}

Complex::~Complex(){
if(p!=NULL)//需要释放内存
delete p;
}

在堆中使用对象

类似在堆中创建一个普通的数据空间,也可以在堆中创建一个类的对象。

同样的在堆中使用完创建的对象之后,需要通过 delete 释放相应的存储空间。

下面的程序中:一个是在栈(主函数)中使用对象,一个是程序员自主在堆中使用对象

由于是在堆中创建的对象,同样只能通过指针间接访问

  • 指针解除引用,通过 . 运算符访问 (*pComplex).setReal(3);

  • 通过指针的指向运算符 -> 直接访问,更加方便 pComplex->setReal(3)

成员函数

成员函数是通过调用类的实例的内部函数,这个函数既可以访问自身 private ,还可以访问同类的其他实例的 private

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
#include<bits/stdc++.h>
using namespace std;

class Complex
{
public:
Complex(double r=0,double i=0):real(r),imag(i){}
int getReal(){return real;}
int getImag(){return imag;}
void output(){cout<<real<<"+"<<imag<<"i";}
Complex add(Complex &a);//add()是成员函数,调用add()计算Complex对象与a对象的和
private:
double real,imag;
};

Complex Complex::add(Complex &a){
//add()作为Complex类内部的成员函数,可以访问本类的private,所以直接将a的属性获取和自身的类对象属性进行计算,也可以调用内部函数getImag()和getReal()间接访问
double newr,newi;
newr=real+a.real;
newi=imag+a.imag;
return Complex(newr,newi);
}

int main()
{
Complex c1(1,2),c2(4,6),c3;
c3=c1.add(c2);//只需要一个参数,因为是调用一个类对象的函数和另一个对象计算,所以只需要传入另一个对象
c3.output();//输出:5+8i
return 0;
}

常成员函数( const

定义:类中某些函数只会读取数据成员,而不会修改数据成员,作为一种良好的契约和习惯,应该将该成员函数声明为常成员函数

方法:在函数头的结尾加上 const

作用:在常成员函数内部,如果直接修改数据成员(或者是调用非常成员函数间接修改数据成员)编译器都会报错

常对象

  • 声明为常量的对象,其数据成员不能被修改
  • 通过常对象只能访问常成员函数,不能调用非常成员函数,但是可以调用公有的静态成员函数
  • 常引用和指向常对象的指针也不能修改成对象的数据成员

注意:指向常对象的指针和指向对象的常指针是不一样的

判断下列哪些是正确的操作:

  • 第一个是常成员函数:可以调用只读数据成员的函数 getValue() ,但是不可以调用修改数据成员的函数 setValue()
  • 第二个是指向常对象的指针,不可以调用修改数据成员函数 setValue() 修改对象的参数。但是指针指向的对象可以修改
  • 第三个是指向对象的常指针,由于对象不是常对象,所以指针可以修改指向对象的参数。但是指针本身不可以修改

常指针和指向常量的指针也是很好区分的:看 *const 谁离指针变量名近,离的近的就是修饰指针变量的

1
2
3
4
5
//以下两种为指向常量的指针
const char *p;
char const *p;
//以下为常指针
char * const p;

友元函数

友元是出于执行效率的考虑,允许类外部的函数或其他的类通过对象名直接访问本类的私有成员称为友元函数或友元类。

注意:友元打破了封装和数据隐藏,除非一些特殊场合,应该慎重考虑

规则:友元函数是类外部的函数,但是声明是必须在类的内部完成,需要在声明的函数前加上 friend 关键字。

友元函数的声明放在私有部分和公有部分的作用效果是一样的

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
31
32
#include<bits/stdc++.h>
using namespace std;

class Complex
{
public:
Complex(double r=0,double i=0):real(r),imag(i){}
int getReal(){return real;}
int getImag(){return imag;}
void output(){cout<<real<<"+"<<imag<<"i";}
friend Complex add(Complex &a,Complex &b);
//add()是友元函数,可以直接通过本类对象名访问该类的private
private:
double real,imag;
};

Complex add(Complex &a,Complex &b){
//友元函数不是类的成员函数,所以不用加类名限定符,而且需要传入两个参数了
double newr,newi;
newr=b.real+a.real;
newi=b.imag+a.imag;
return Complex(newr,newi);
}

int main()
{
Complex c1(1,2),c2(4,6),c3;
c3=add(c1,c2);
//只需要一个参数,因为是调用一个类对象的函数和另一个对象计算,所以只需要传入另一个对象
c3.output();//输出:5+8i
return 0;
}

若是代码中 add 不是友元函数(即普通函数),那么作为一个类的外部函数则不能直接访问 private 了,所以只能通过 getImag()getReal() 两个类的公有函数接口间接访问private

普通函数

声明和定义均在类的外部(说白了就是普通的函数),那么它就不能访问 private ,只能访问 public 成员

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
#include<bits/stdc++.h>
using namespace std;

class Complex
{
public:
Complex(double r=0,double i=0):real(r),imag(i){}
int getReal(){return real;}
int getImag(){return imag;}
void output(){cout<<real<<"+"<<imag<<"i";}
private:
double real,imag;
};

Complex add(Complex &a,Complex &b);//定义

Complex add(Complex &a,Complex &b){//声明
double newr,newi;
newr=b.getReal()+a.getReal();//通过public的函数间接访问private
newi=b.getImag()+a.getImag();
return Complex(newr,newi);
}

int main()
{
Complex c1(1,2),c2(4,6),c3;
c3=add(c1,c2);//只需要一个参数,因为是调用一个类对象的函数和另一个对象计算,所以只需要传入另一个对象
c3.output();//输出:5+8i
return 0;
}

静态成员

static 关键字在 C 语言中常常用到。在程序中的任何一个函数内普通的静止变量默认是不会初始化的,而且这个变量存储于进程栈的空间,使用完毕就会释放。如果在其前面加上 static 关键字变量,那么这个变量就会被放入全局数据区分配内存并初始化为 0(相当于变成了全局变量),即使函数返还它的值也会保持不变。

同样,也可以用 static 关键字这一特性在面向对象中使用:

在写类和对象的作业是发现这个关键字的优势,通过一道题来说明+介绍

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
class Student{
public:
int id,score;
static int count;//用static关键字声明为全局变量
Student(int x,int y);
Student(Student &x);
void display();
};

int Student::count=0;//定义

Student::Student(int x,int y){
id=x,score=y;
count++;
}

Student::Student(Student &x){
id=x.id+1;score=x.score;
count++;
}

void Student::display(){
cout<<id<<" ";
if(score)cout<<"Pass"<<endl;
else cout<<"Fail"<<endl;
}

count() 函数是 Student 类的内部成员,负责统计录入的学生个数,也就是相当于整个程序都是在用一个count来计数(相当于独一无二的全局变量),若是直接当做普通的内部成员在默认构造函数中初始化为 0 ,那么类每实例化一个就会产生新的一个 count(初始化为 0 ),无法满足题目的要求

静态数据成员

在类内数据成员的声明前加上 static 关键字,该数据成员就是类内的静态数据成员。其特点如下:

  • 静态数据成员存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中定义(在类的内部先声明)
  • 静态数据成员是类的成员,不管内存是否存在对象或者多少个对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝
  • 由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作
  • 和普通数据成员一样,静态数据成员也遵从 publicprotectedprivate 访问规则

公有静态成员:可以在成员函数中随意访问,也可以通过类名称访问

私有成员函数:不能通过类名访问,但是可以借助对象调用公有成员函数接见访问

  • 静态数据成员的初始化格式<数据类型><类名>::<静态数据成员名>=<值>
  • 类的静态数据成员有两种访问方式<类对象名>.<静态数据成员名><类类型名>::<静态数据成员名>

同全局变量相比,使用静态数据成员有两个优势:

  • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性
  • 可以实现信息隐藏。静态数据成员可以是 private 成员,而全局变量不能
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
31
32
33
#include<bits/stdc++.h>
using namespace std;

class Dog
{
public:
static int cnt;//公有静态成员
//通过调用构造函数和析构函数进行统计
Dog(){
cnt++;
}
~Dog(){
cnt--;
}
string getName();
void setName(string name);
...
private:
string name;
};

int Dog::cnt=0;//公有成员统通过类名访问初始化:计小狗的数量

int mian()
{
Dog dog1;
cout<<dog1.cnt<<endl;
Dog *dog2=new Dog();
cout<<dog2->cnt<<endl;
delete dog2;
cout<<Dog::cnt<<endl;
return 0;
}

静态成员函数

引入:多个对象”共享“同一个静态数据成员,可以随意访问,容易造成混乱。解决办法:将静态数据成员定义为 private ,通过公有的静态成员函数访问

与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象,其特性如下:

  • 静态成员函数没有 this 指针,它无法访问属于类对象的非静态数据成员(普通函数都不行),也无法访问非静态成员函数,它只能调用其余的静态成员函数
  • 出现在类体外的函数定义不能指定关键字 static
  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员
1
2
3
4
5
6
7
8
9
class Test
{
private:
static int x,y,z;
public:
static int sum(){
return x+y+z;
}
};

对象成员

  • 在创建复杂类的时候,经常讲简单的类对象作为其成

  • 简单类组合构成符合类,构成了存在某些关系的类

  • 当一个类的对象作为另一个类的成员时,称该对象为对象成员,这种方法为“组合技术”

  • 包含对象作为数据成员的类成为容器类,当创建容器类对象时,对象成员的初始化由构造函数的初始化列表提供

比如要是设计“计算一个关于三角形类的距离“程序,那么我可以先定义关于点的类(保存关于点的参数),然后在三角形类定义的时候调用点的类的头文件和计算距离函数。客户端的程序只用把包装好的上述的程序调用头文件。

了解即可

好处:如果由二维空间转向三维空间,客户端不是透明的用户只用关心Trangle类的接口而不关心其实现,只需要更改那几个头文件的参数。

运算符重载

引入:表达式 9/2=49.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++ 的强大功能和特色,多数运算符都能够重载,但是有一些约束条件。

  • 不允许重载内置类型的运算符
  • 不能改变运算符的优先级和目数
  • 不能创建新的运算符,如将 * 声明为指数运算符
  • 改变运算符的语义是合法的,但是重载 + 运算符作减法运算会导致代码难以理解。重载运算符的初衷就是便于使用和理解

可/不可重载运算符

下面是可重载的运算符列表:

类别 符号
双目算术运算符 + (加),-(减),*(乘),/(除),%(取模)
关系运算符 ==(等于),!=(不等于),< (小于),> (大于),<=(小于等于),>=(大于等于)
逻辑运算符 ||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符 + (正),-(负),*(指针),&(取地址)
自增自减运算符 ++(自增),--(自减)
位运算符 | (按位或),&(按位与),~(按位取反),^(按位异或),<<(左移),>>(右移)
赋值运算符 =+=-=*=/=% =&=|=^=<<=>>=
空间申请与释放 newdeletenew[]delete[]
其他运算符 ()(函数调用),->(成员访问),,(逗号),[](下标)

下面是不可重载的运算符列表:

  • . :成员访问运算符
  • .-> :成员指针访问运算符
  • :: :域运算符
  • sizeof :长度运算符
  • ?: :条件运算符
  • **# : **预处理符号

加法运算符重载 +

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
class Complex
{
private:
double real,imag;
public:
Complex(double r=0,double i=0)
:real(r),imag(i){}
Complex(const Complex& c)
:real(c.real),imag(c.imag){}
void output() const;
Complex operator+(const Complex &c) const;
//重载Complex类的+运算符,operator相当于函数名
};

Complex Complex::operator+(const Complex &c) const{
return Complex(real+c.real,imag+c.imag;)
}

int main()
{
Complexa a(3,4),b(4,5),c;
a.output();
b.output();
c=a+b;
c.output();
return 0;
}

a+b相当于 a.operator+(b) ,返还临时复制对象并将临时复制对象赋值给 c

复合运算符重载 +=

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
class Complex
{
private:
double real,imag;
public:
Complex(double r=0,double i=0)
:real(r),imag(i){}
Complex(const Complex& c)
:real(c.real),imag(c.imag){}
void output() const;
Complex operator+=(const Complex &c) const;
//重载Complex类的+运算符,operator相当于函数名
};

Complex Complex::operator+=(const Complex &c) const{
this->real+=c.real;
(*this).imag+=c.imag;
return *this;
}

int main()
{
Complexa a(3,4),b(4,5),c;
a.output();
b.output();
a+=b;
a.output();
return 0;
}

a+=b相当于 a.operator+=(b) ,返还 a 的引用

赋值运算符=

  • 相同类型之间赋值时实际上也是通过调用 operator= 冲原函数实现的。如果类中没有定义该函数,编译器会提供默认的赋值运算符函数,实现对象数据成员之间的逐一赋值。
  • 如果类中只包含简单的数据成员,默认的赋值运算符足以完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Complex
{
private:
double real,imag;
public:
Complex(double r=0,double i=0)
:real(r),imag(i){}
Complex(const Complex& c)
:real(c.real),imag(c.imag){}
void output() const;
Complex operator+(const Complex &c) const;
Complex& operator=(const Complex &c);
//重载Complex类的+运算符,operator相当于函数名
};

Complex Complex::operator+(const Complex &c) const{
return Complex(real+c.real,imag+c.imag;)
}

Complex& Complex::operator=(Complex &c){
real=c.real;
imag=c.imag;
return *this;
}

对于简单的类(不含有指针成员),无需重载 operator= 运算符,利用缺省的赋值功能也能够正常工作。

  • 重载为类的成员函数 arguement-list 中参数的个数比原 operator_symble 所需要的参数个数少一个(后置 ++-- 除外)
  • 重载了类的友元函数 arguement-list 中参数的个数与原 operator_symble 所需要的的参数个数相同

输入/输出运算符 >> / <<

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Counter{
public:
Counter():value(0){}
~Counter(){}
int getValue() const{
return value;
}
void setValue(int x){
value=x;
}
friend ostream& operator<<(ostream& out,const Counter& c);
private:
int value;
};

ostream& operator<<(ostream& out,const Counter& c){
out<<c.getValue()<<endl;
return out;
}
  • 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
2
3
4
5
6
7
8
9
//输入
cin>>s;//不接受空格,回车,Tab等
scanf("%s",s);//同上
getline(cin,s);//接受空格,Tab
//复制
string s1(s2);
//拼接
string s3=s1+s2;
s3=" "+s3+" ";

putsgets 是以指针形式存入,只是用于字符数组,可以接受空格、Tab

常用成员函数

1
2
3
4
5
6
7
8
9
10
11
12
//查找字符和子字符串
s.find("day",0);//从0开始查找子字符串
s.find('a',5);//没找到返还nops(-1)

//截短字符串
s.erase("days");//直接截取指定字符串
s.erase(s1);
s.erase(13,25);//相当于数组形式截取
s.erase(beg,end);//在[beg,end)之间截取(迭代器)

//直接清空
s.clear();

常用的函数

1
2
3
4
5
6
//反转函数
reverse(beg,end);//将[beg,end)区间(迭代器)的字符串反转

//大小写转换
transform(beg,end,::toupper);
transform(beg,end,::tolower);

STL动态数组类

动态数组让程序员能够灵活的存储数据,无需在编写程序的时候就知道数组的长度

std::vector

特点:提供了动态数组的通用功能

  • 在数组末尾添加元素的时间是固定的,不随数组大小而异,删除亦然
  • 在数组的中间添加或者删除元素所需的时间是与该元素后面的元素个数成正比
  • 存储的元素是动态的,vector负责管理内存
  • 头文件#include<vector>

访问

  1. at()

    v.at(pos) 返回容器中下标为 pos 的引用。如果数组越界抛出 std::out_of_range 类型的异常。

  2. operator[]

    v[pos] 返回容器中下标为 pos 的引用。不执行越界检查。

  3. front()

    v.front() 返回首元素的引用。

  4. back()

    v.back() 返回末尾元素的引用。

  5. data()

    v.data() 返回指向数组第一个元素的指针。

迭代器

  1. begin()/cbegin()

    返回指向首元素的迭代器,其中 *begin = front

  2. end()/cend()

    返回指向数组尾端占位符的迭代器,注意是没有元素的。

  3. rbegin()/rcbegin()

    返回指向逆向数组的首元素的逆向迭代器,可以理解为正向容器的末元素。

  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义具有10个整型元素的向量(尖括号为元素类型名,它可以是任何合法的数据类型),不具有初值,其值不确定
vector<int> a(10);

//定义具有10个整型元素的向量,且给出的每个元素初值为1
vector<int> a(10,1);

//定义向量存有三个整形元素的向量,列表初始化形式
vector<int> a{202,2017,-1};

//用向量b给向量a赋值,a的值完全等价于b的值
vector<int> a(b);

//将向量b中从0-2(共三个)的元素赋值给a,a的类型为int型
vector<int> a(b.begin(),b.begin+3);

//从数组中获得初值
int b[7]={1,2,3,4,5,6,7};
vector<int> a(b,b+7);

常用成员函数

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//清空a中的元素
a.clear();

//判断a是否为空,空则返回true,非空则返回false
a.empty();

//删除a向量的最后一个元素
a.pop_back();

//删除a中第一个(从第0个算起)到第二个元素,也就是说删除的元素从a.begin()+1算起(包括它)一直到a.begin()+3(不包括它)结束
a.erase(a.begin()+1,a.begin()+3);

//在a的最后一个向量后插入一个元素,其值为5
a.push_back(5);

//在a的第一个元素(从第0个算起)位置插入数值5,
a.insert(a.begin()+1,5);

//在a的第一个元素(从第0个算起)位置插入3个数,其值都为5
a.insert(a.begin()+1,3,5);

//b为数组,在a的第一个元素(从第0个元素算起)的位置插入b的第三个元素到第5个元素(不包括b+6)
a.insert(a.begin()+1,b+3,b+6);

//返回a中元素的个数
a.size();

//返回a在内存中总共可以容纳的元素个数
a.capacity();

//将a的现有元素个数调整至10个,多则删,少则补,其值随机
a.resize(10);

//将a的现有元素个数调整至10个,多则删,少则补,其值为2
a.resize(10,2);

//将a的容量扩充至100,
a.reserve(100);

//b为向量,将a中的元素和b中的元素整体交换
a.swap(b);

//b为向量,向量的比较操作还有 != >= > <= <
a==b;

vector 重写了比较运算符及赋值运算符

vector 重载了六个比较运算符,以字典序实现,这使得我们可以方便的判断两个容器是否相等(复杂度与容器大小成线性关系)。例如可以利用 vector 实现字符串比较(当然,还是用 std::string 会更快更方便)。另外 vector 也重载了赋值运算符,使得数组拷贝更加方便。

添加元素的方法

1.向向量a中添加元素

1
2
vector<int>a;
for(int i=0;i<10;++i){a.push_back(i);}

2.从数组中选择元素向向量中添加

1
2
3
int a[6]={1,2,3,4,5,6};
vector<int> b;
for(int i=0;i<=4;++i){b.push_back(a[i]);}

3.从现有向量中选择元素向向量中添加

1
2
3
4
5
6
int a[6]={1,2,3,4,5,6};
vector<int>b;
vector<int>c(a,a+4);
for(vector<int>::iterator it=c.begin();it<c.end();++it{
b.push_back(*it);
}

4.从文件中读取元素向向量中添加

1
2
3
4
5
ifstream in("data.txt");
vector<int>a;
for(int i;in>>i){
a.push_back(i);
}

常见错误赋值方式

1
2
3
4
vector<int>a;
for(int i=0;i<10;++i){
a[i]=i;
}//下标只能用来获取已经存在的元素

从向量中读取元素

1.通过下标方式获取

1
2
3
4
5
int a[6]={1,2,3,4,5,6};
vector<int>b(a,a+4);
for(int i=0;i<=b.size()-1;++i){
cout<<b[i]<<endl;
}

2.通过迭代器方式读取

1
2
3
int a[6]={1,2,3,4,5,6};
vector<int>b(a,a+4);
for(vector<int>::iterator it=b.begin();it!=b.end();it++){cout<<*it<<" ";}

常用的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<algorithm>

//对a中的从a.begin()(包括它)到a.end()(不包括它)的元素进行从小到大排列
sort(a.begin(),a.end());

//对a中的从a.begin()(包括它)到a.end()(不包括它)的元素倒置,但不排列,如a中元素为1,3,2,4,倒置后为4,2,3,1
reverse(a.begin(),a.end());

//把a中的从a.begin()(包括它)到a.end()(不包括它)的元素复制到b中,从b.begin()+1的位置(包括它)开始复制,覆盖掉原有元素
copy(a.begin(),a.end(),b.begin()+1);

//在a中的从a.begin()(包括它)到a.end()(不包括它)的元素中查找10,若存在返回其在向量中的位置
find(a.begin(),a.end(),10);

std::deque

vector 的不同就在于支持在数组开头和末尾插入或者删除元素,需要包含头文件#include<deque>

补充

vector的内置函数std::deque同样适用,而且还可以用push_front()pop_front()在开头和结尾插入删除元素

1
2
3
4
5
//在a的开头向量前插入一个元素,其值为5
a.push_front(5);

//删除a的第一个元素
a.pop_front();

STL链表

std::list

std::list 是 STL 提供的双向链表数据结构。能够提供线性复杂度的随机访问,以及常数复杂度的插入和删除。

特点

  • 链表是由一系列节点组成,其中每个节点除包含对象或者值外还指向下一个节点的位置,双向链表还指向前一个节点的位置。
  • 链表允许从开头、中间和结尾以固定的时间插入元素
  • std 命名空间中的模板 list 是一种泛型实现,要使其成员函数需要实例化模板
  • 动态存储不存在浪费空间和溢出
  • 头文件 include<list>

迭代器

std::vector 一样

初始化

1
2
3
4
5
6
7
8
9
list<int> c0; //空链表

list<int> c1(3); //建一个含三个默认值是0的元素的链表

list<int> c2(5,2); //建一个含五个元素的链表,值都是2

list<int> c4(c2); //建一个c2的copy链表

list<int> c5(c1.begin(),c1.end()); ////c5含c1一个区域的元素[_First, _Last)。

常用成员函数

list也可以像 std::vector 一样用 =>< 等运算符,好像其他容器也应该是被重载了容器,有待考究。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
c.assign(n,num);//将n个num拷贝给链表c

c.assign(beg,end);//将[beg,end)区间(迭代器)元素拷贝赋值给链表c

c.front();//返还链表的第一个元素

c.back();//返还链表的最后一个元素

c.empty();//判断链表是否为空

c.size();//返还链表的元素个数

c.max_size();//返还链表可能容纳的最大元素个数

c.clear();//清空链表的所有元素从

c.insert(pos,num);//在pos位置插入num

c.insert(pos,n,num);//在pos位置插入n个num元素

c.insert(pos,beg,end);//在pos位置插入区间为[beg,end)的元素

c.erase(pos);//删除pos位置的元素

c.push_back(num);//在末尾增加一个元素

c.pop_back();//删除末尾的元素

c.push_front(num);//在开始位置增加一个元素

c.pop_front();//删除第一个元素

c.resize(n);//从新定义链表的长度,超出原始长度的部分用0代替,小于的部分删除

c.resize(n,num);//从新定义链表的长度,超出原始长度的部分用num代替

c1.swap(c2);//将c1和c2交换

swap(c1,c2) ;//同上

c1.merge(c2);//合并两个有序的链表并使之有序,从新放到c1中并释放c2

c1.merge(c2,cmp);//同上并将合并的链表按照cmp()比较函数的规则排序

c1.splice(c1.beg,c2);//将c2连接在c1的beg位置并释放c2

c1.splice(c1.beg,c2,c2.beg);//将c2的beg位置的元素连接到c1的beg位置并在c2中释放掉beg位置的元素

c1.splice(c1.beg,c2,c2.beg,c2.end);// 将c2的[beg,end)位置的元素连接到c1的beg位置并且释放c2的[beg,end)位置的元素

c.remove(num);//删除链表中匹配num的元素

c.remove_if(cmp);//删除条件满足的元素,参数为自定义的回调函数

c.reverse();//反转链表

c.unique();//删除相邻的元素

c.sort();//链表按照升序排序

c.sort(cmp);//链表按照比较函数排序

注意:sort() 函数是不能给 list 排序的,因为 list 迭代器好像不一样( random类型 ),要用 list 自己的 sort() 成员函数

std::forward_list

C++11 引入了 std::forward_list ,它是单向链表,只允许沿着一个方向遍历

需要包含 include<forward_list>头文件

特点

  • 头文件:#include<fstream>
  • 采用的是头插法,只能用 push_front()

说明:因为单向链表只用记录后一个节点的位置了,所以相对于双向链表来说节省了很多的空间

STL集合类

STL向程序员提供了一些容器类,一遍在应用程序内进行频繁而快速的搜索。std::setstd::multiset 用于存储一组经过排序的元素,其查找元素的复杂度为对数,而 unordered 集合的插入和查找的时间是固定的。

简介

容器 setmultiset 让程序员能够在容器中快速查找键,键是存储在一维容器中的值。setmultiset 之间的区别在于后者存储重复的值,而前者只能存储唯一的值。

为了快速实现搜索,他们的内部结构都是类似二叉树的红黑树。在将元素插入容器的时候是进行排序的,从而提高的查找速度。这也就意味着:

  • set 不能像 vector 那样可以使用其他的元素替换给定位置的值
  • set 存储式去重比较后再插入
  • 必须使用头文件 #include<set>

下面以 set 容器讲解

初始化

1
2
3
4
5
6
7
8
9
10
11
set<int> seta; //默认是小于比较器less<int>的set

set<int,greater<int> > setb; //创建一个带大于比较器的set,需包含头文件functional

int a[5]={1,2,3,4,5};
set<int> setc(a,a+5); //数组a初始化一个set;

set<int> setd(setc.begin(),setc.end()); //setc初始化一个set
//上述两例均为区间初始化

set<int> sete(setd); //拷贝构造创建set

常用成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//插入
seta.insert(x);//插入x值
seta.insert(setb.begin(),setb.end());//将setb容器区间的值插入

//删除
seta.erase(x);//删除值为x的所有值并返还删除的个数
seta.erase(pos);//另一个版本,删除迭代器pos所指向的值
seta.erase(beg,end);//删除区间[beg,end)(迭代器)的值
seta.clear();//清空

//查找
seta.find(x);//查找并返还第一个x值的位置,没有返还seta.end()

//统计
seta.count(x);//统计x值的个数

在需要频繁查找的应用程序中(多次用到 find() 操作),setmultiset 很有优势,因为在插入的时候已经排好顺序,查找速度很快。但是因此也导致插入时需要花费额外的时间

STL散列集合

相交于未经排序的容器(查找时间和元素数成正比),排好序的容器极大地改善了性能。前辈不满于现状,通过努力还把集合类容器的插入和排序时间变为了固定值。std::unordered_setstd::unordered_multiset 是基于散列实现的容器,即使用散列函数来计算索引。将元素插入散列集合时,首先使用散列函数计算出唯一的索引,然后在根据索引决定将元素放入哪一个桶中

  • 需要使用头文件 #include<unordered_set>

使用上还有点区别,现在看不懂会在在写吧

使用 std::fstream 处理文件

C++ 提供 std::fstream 旨在以独立平台的方式访问文件。std::fstreamstd::ofstream 那里继承了写入文件的功能,并从 std::ifstream 那里继承了读入文件的功能。

所有的操作对象都是针对于内存的,std::ifstream 是从文件传入内存,std::oftream 是从内存传出文件

使用 open()close() 打开关闭文件

fstream 类中,有一个成员函数 open(),就是用来打开文件的,其原型是:

1
2
3
4
5
6
7
8
fstream myFile;
myFile.open("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
//或者写成一句话(构造函数)
if(!myFile){
exit(1);//打开不成功返还1并结束进程
}
//...一系列操作
myFile.close();//一定要记得关闭文件和内存的联系保护文件

一般路径要提供从所在根目录开始的路径,若是没有路径系统默认是当前目录设置。

使用 open() 后都要先判断文件是否成功打开,只有打开成功了才使用文件流对象,一定要记得结束执行程序前关闭文件和内存的联系保护文件

1
2
if(myFile)//成功打开返还非0值
if(myFile.isopen())//同上

打开文件的方式在类 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
2
3
output << "I Love You";//向文件输出字符串"I Love You"
string word;
input >> word;//从文件输入一个整数值。

这种方式还有一种简单的格式化能力,比如可以指定输出为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _Student{
string name;
int age;
}Student;
unsigned char str1[]="I Love You";
int n[5];
ifstream in("xxx.xxx");
ofstream out("yyy.yyy");
out.write(str1,strlen(str1));//把字符串str1全部写到yyy.yyy中
Student stu;
stu.name = "Jack";
stu.age = 18;
out.write(reinterpret_cast<char *>(&stu), sizeof(stu) );
//这里读写是无论是什么数据类型都需要转换为char型,所以用到了reinterpret_cast<char *>
in.read((unsigned char*)n,sizeof(n));//从xxx.xxx中读取指定个整数,注意类型转换
in.close();out.close();

3.检测 EOF

这里真的很重要,要理解的是文件读入的时候只有读到终止符(不能输出的一个值)的时候,才会停止

eof() 检查文件是否到达终止符,是的话就返还 1 ,否为返还 0

错误样例

1
2
3
4
while(!fin.eof()){
fin.get(ch);
cout<<ch;
}

错因:当 fin.get() 读到了终止符的时候,还是输出了,但是因为终止符是一个不能输出的值,所以程序会再次输出文件的最后一个可输出的值

正确方法

1
2
3
4
5
fin.get(ch);
while(!fin.eof()){
cout<<ch;
fin.get(ch);
}
1
2
3
4
5
while(!fin.eof()){
fin.get(ch);
if(fin.eof())break;
cout<<ch;
}

stringstream 转换数据类型/切割字符串

string 字符串

写着部分主要是为了整体上了解 stringchar[] 的不同之处

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 )物件,和之前学过的 iostreamfstream 有类似的操作方式。要使用 stringstream , 必須先加入這一行:#include <sstream>

<sstream> 定义了三个类:istringstreamostringstreamstringstream ,分别用来进行流的输入、输出和输入输出操作。本文以 stringstream 为主,介绍流的输入和输出操作。

<sstream> 主要用来进行数据类型转换,由于 <sstream> 使用 string 对象来代替字符数组( snprintf 方式),就避免缓冲区溢出的危险;而且,因为传入参数和目标对象的类型会被自动推导出来,所以不存在错误的格式化符的问题。简单说,相比 C 库的数据类型转换而言,<sstream> 更加安全、自动和直接。

使用 stringstream 对象简化类型转换

数据类型转换

示例展示的是把 int 类型转换为 string 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <string>
#include <sstream>
#include <iostream>
#include <stdio.h>

using namespace std;

int main()
{
stringstream sstream;
string strResult;
int nValue = 1000;

// 将int类型的值放入输入流中
sstream << nValue;
// 从sstream中抽取前面插入的int类型的值,赋给string类型
sstream >> strResult;

cout << "[cout]strResult is: " << strResult << endl;
printf("[printf]strResult is: %s\n", strResult.c_str());

return 0;
}

可以看到和 fstream 还是有一定区别的,stringstream 缓存区放在左侧,其他类型放在右侧,传入用 <<,传出用 >>

相比传统 C 的转换 stringstream 的使用更加安全遍历,不用可以考虑内存的问题

多个字符拼接

本示例介绍在 stringstream 中存放多个字符串,实现多个字符串拼接的目的(其实完全可以使用string类实现),同时,介绍 stringstream 的清空方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
stringstream sstream;

// 将多个字符串放入 sstream 中
sstream << "first" << " " << "string,";
sstream << " second string";
cout << "strResult is: " << sstream.str() << endl;

// 清空 sstream
sstream.str("");
sstream << "third string";
cout << "After clear, strResult is: " << sstream.str() << endl;

return 0;
}
  • 可以使用str()方法,将stringstream 类型转换为 string 类型;
  • 可以将多个字符串放入 stringstream 中,实现字符串的拼接目的;
  • 如果想清空 stringstream ,必须使用 sstream.str(""); 方式;clear() 方法适用于进行多次数据类型转换的场景。

stringstream 的清空

清空 stringstream 有两种方法:clear() 方法以及 str("") 方法,这两种方法有不同的使用场景。str("")方法的使用场景,在上面的示例中已经介绍了,这里介绍 clear() 方法的使用场景。

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
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
stringstream sstream;
int first, second;

// 插入字符串
sstream << "456";
// 转换为int类型
sstream >> first;
cout << first << endl;

// 在进行多次类型转换前,必须先运行clear()
sstream.clear();

// 插入bool值
sstream << true;
// 转换为int类型
sstream >> second;
cout << second << endl;

return 0;
}

在本示例涉及的场景下(多次数据类型转换),必须使用 clear() 方法清空 stringstream ,不使用 clear() 方法或使用 str("") 方法,都不能得到数据类型转换的正确结果

使用 stringstream 对象切割字符

stringstream 的另一个妙处就是按空格分隔字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
using namespace std;

int main()
{
stringstream sstream;
string s, str;
getline(cin, s);
sstream<<s;
while(sstream>>str)
cout<<str<<endl;
return 0;
}

/*
输入:asd asd asd
输出:asd
asd
asd
*/