C++基础知识(1)

C++面向对象三大特性

封装,继承,多态

封装

就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可以信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。实例:将函数定义和函数声明放在不同的文件中。

继承

指可以让某个类型的对象获得另一个类型的对象的属性的方法。它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。

继承概念的实现方式有两类:1 实现继承○ 2 接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必需提供实现的能力。

公有继承(public)

派生类的构造函数

必须首先创建基类对象,如果不调用基类构造函数,程序会使用默认的基类构造函数。除非要使用默认构造函数,否则应该显式调用正确的基类构造函数。

派生类与基类的特殊关系

(1)派生类可以使用基类的方法,条件是方法不是私有的。

(2)基类指针/引用可以在不进行显式类型转换的情况下指向派生类对象。但是基类指针/引用只可以调用基类方法。(在未引入虚函数前)

(3)派生类指针和引用不可以指向基类对象

(4)引用兼容性可以实现将基类对象初始化为派生类对象。

(5)可以将派生类对象赋给基类对象。

(6)在派生类中方法可以调用基类的同名方法,方法是使用作用域解析运算符。

为什么析构函数定义为虚的?

通常给基类搞一个虚析构函数,即使它并不需要析构函数。

友元函数不可以定义为虚函数,因为它不是成员函数。

该函数不是基类的友元函数,因此不可以访问基类的私有数据成员,但可以使用强制类型转换把派生类转换为基类,然后调用基类的友元函数来访问派生类中基类的成员,然后再单独输出派生类的成员即可。

关于虚函数

如果方法是通过引用或指针而不是对象调用的

1)如果方法在基类中被定义为虚函数,程序将根据引用或指针指向的对象的类型来选择方法。

2)如果方法在基类中没有定义为虚函数,程序将根据引用或指针类型选择方法。

将派生类引用或指针转换为基类引用或指针被称为向上强制类型转换,这使得公有继承不需要进行显式类型转换。

相反,将基类指针或引用转换为派生类指针或引用,即向下强制转换,一般来说是不允许的。

私有继承(private

保护继承(protected

关键字protectedprivate相似,即在类外只可以用公有类成员来访问protected部分中的类成员。Privateprotected的区别只有在基类派生的类中才会表现出来。派生类成员可以直接访问基类的保护成员,但不可以直接访问基类的私有成员。

对于外部世界来说,保护成员的行为和私有成员的行为类似。对于内部世界来说,保护成员与公有成员类似。

不可以被继承的函数

1)构造函数

其他函数都可以通过类对象来调用,但是构造函数是用来产生对象的函数,它在对象之前。而继承对对象来说是能够调用父类的函数。在创建派生类对象时,必须调用派生类的构造函数,然而派生类构造函数通常使用成员初始化列表语法显式调用基类函数,以创建派生类对象的基类部分,然后如果构造函数可以被继承,那么这个时候需要派生类对象去调用,然后我们正是在构建派生类对象啊·!

2)析构函数

3)赋值运算符

继承的内存分配(当基类使用动态内存分配时)

1 派生类不使用new

不需要为派生类定义显式析构函数/复制构造函数/赋值运算符

(1)析构函数

(2)复制构造函数

默认复制构造函数执行成员复制,这对于动态内存分配不合适,但对于派生类中的不属于基类的成员是合适的,因为没有动态分配内存,派生类的默认复制构造函数使用基类的显式复制构造函数来完成基类成员的深度复制。

(3)赋值运算符

类的默认赋值运算符会自动调用基类的显式赋值运算符来对基类组件进行赋值。

2 派生类使用new

需要自己定义显式析构函数/显式复制构造函数/显式赋值运算符。

对于赋值运算符来说,必须使用作用域解析运算符显式地调用基类的赋值运算符。因为派生类不可以直接访问基类的私有成员。

多态

简单来说,C++多态是指在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果是基类,就调用基类的函数。

复杂来说,向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法。

多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。

多态主要通过继承和虚函数实现。

多态: 探索C++多态和实现机理 - tp_16b - 博客园  也含有多继承和菱形继承

多重继承和虚继承: https://www.jb51.net/article/53743.htm

菱形继承和虚继承 https://www.jb51.net/article/53743.htm  基类指针与派生类对象

多态和虚函数: 【C++拾遗】 C++虚函数实现原理_上善若水,人淡如菊-CSDN博客_c++ 虚函数

菱形继承和虚继承的内存布局: 【C++拾遗】 从内存布局看C++虚继承的实现原理_上善若水,人淡如菊-CSDN博客  

虚继承: C++中虚继承的作用及底层实现原理_bxw1992的博客-CSDN博客_虚继承

虚表指针存放位置以及其初始化时刻:虚表指针初始化_疯子小鱼-CSDN博客_虚表指针什么时候初始化

虚函数

当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址,同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。

当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数表指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。

如果有一个基类类型的指针,指向派生类,那么当调用虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调用派生类的虚函数表中的虚函数。以此实现多态。

虚函数与纯虚函数

那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,就是实现多态性(Polymorphism),虚函数必须实现,如果不实现,编译器将报错。

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

它们必须在继承类中重新声明函数,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

为什么要用纯虚函数?

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。

在什么情况下使用纯虚函数(pure vitrual function)?

1,当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;

2,这个方法必须在派生类(derived class)中被实现;

   如果满足以上两点,可以考虑将该方法申明为pure virtual function.

对于派生类,编译器建立虚函数表的过程

(1)拷贝基类的虚函数表,如果是多继承,就拷贝每个有虚函数基类的虚函数表,当然还有一个基类的虚函数表和派生类自身的虚函数表共用了一个虚函数表,也称为某个基类为派生类的主基类;

(2)查看派生类中是否有重写基类中的虚函数,如果有,就替换成已经重写的虚函数地址;

(3)查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中。

析构函数为什么写成虚函数

为了降低内存泄漏的可能性。举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数(该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执行基类的析构函数,派生类的自身内容将无法被析构,造成内存泄漏。如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。

如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数。从地址早绑定还是玩绑定来回答。   如果不是虚函数,那么具体调用那个函数是由指针的类型决定的,而不是指针所指对象的类型决定的。

构造函数为什么不定义为虚函数

(1)违背先实例化后调用的准则。

虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,无法找到对应的虚函数表来调用虚函数

(2)虚函数调用只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要创建一个对象的话,是需要知道对象的完整信息的。特别是,需要知道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;

深拷贝/浅拷贝

拷贝(复制)构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将调用拷贝构造函数。

在未定义显式拷贝构造函数的情况下,系统会调用默认的拷贝函数-即浅拷贝,它能够完成成员的一一复制。

但某些状况下,类内成员变量需要动态开辟堆内存时,比如指针,这个时候需要执行深拷贝,为类成员重新分配空间。否则将会出现2个指针指向同一个地址,当需要析构对象时,会出现野指针,后析构的指针成员指向一个空地址。

深拷贝与浅拷贝之间的区别就在于深拷贝会在堆内存中另外申请空间来存储数据,从而也就解决来野指针的问题。简而言之,当数据成员中有指针时,必需要用深拷贝更加安全。

深拷贝指拷贝时对象资源重新分配,两个对象的资源内存不同,释放一个对象资源不会影响另一个。

浅拷贝指两个对象均指向同一内存空间,释放一个对象的资源,另一个对象的资源也没了,容易生成野指针。

 什么情况下会调用拷贝构造函数(三种情况)

类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数:

1)一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间中。

2)一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值。

3)一个对象需要通过另外一个对象进行初始化。

为什么拷贝构造函数必须使用引用传递,而不可以是值传递。

为了防止递归调用。当一个对象需要以值方式进行传递时,编译器会生成代码调用它的拷贝构造函数生成一个副本,如果类 A 的拷贝构造函数的参数不是引用传

递,而是采用值传递,那么就又需要为了创建传递给拷贝构造函数的参数的临时对象,而又一次调用类 A 的拷贝构造函数,这就是一个无限递归。

不可以使用memset初始化类对象

因为类具有虚函数的概念,当类声明了虚函数时,会有一个虚函数表指针指向类对象的虚函数表,虚函数表中存储了所有的函数的地址。调用memset会把这个隐藏的指针置0,当程序运行时,找不到函数地址会崩溃运行。为了避免这种情况,记住对于有虚拟函数的类对象,决不能使用memset来进行初始化操作。而是要用缺省的构造函数来初始化成员变量。

C++四种类型转换

1 Const_cast(是唯一一个可以操作常量的转换符)

功能:修改类型的const或volatile属性。使用该运算方法可以返回一个指向非常量的指针(或引用),就可以通过该指针(或引用)对它的数据成员任意改变。

用法:const_cast<type_id> (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。

一、常量指针被转化成非常量的指针,并且仍然指向原来的对象;

二、常量引用被转换成非常量的引用,并且仍然指向原来的对象;

三、const_cast一般用于修改底指针。如const char *p形式。

提供该运算符的原因:有时候需要这样一个值,在大多数时是常量,有时又是可以修改的。在这种情况下可以把值声明为const,在需要修改时使用const_cast强制转换。

输出的值都是300

输出num是10,p1,p2都是20.大佬们说是常量优化了,取值还是从寄存器中取,但寄存器只读,不会更新。

顺便说下volatile

volatile是一个类型修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

简单地说就是防止编译器对代码进行优化。

精确地说就是,编译器在用到这个变量时必须每次都小心地重新从内存中读取这个变量的值,而不是使用保存在寄存器里的备份。

一般说来,volatile用在如下的几个地方:

1中断服务程序中修改的供其它程序检测的变量需要加volatile

2、多任务环境下各任务间共享的标志应该加volatile;

3存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

几个问题

1)一个参数既可以是const还可以是volatile吗?解释为什么。

是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2)一个指针可以是volatile 吗?解释为什么。

是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。

上边的程序加了一个volatile,最后所有的输出都是20,因为指向同一块内存。

2 Static_cast

static_cast是一个计算机函数,功能是把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。

用法: static_cast < type-id > ( expression )

它主要有如下几种用法:

1)用于类层次结构中基类(父类)和派生类(子类)之间指针引用的转换。

进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;

进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

(2)用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

(3)把空指针转换成目标类型的空指针。

(4)把任何类型的表达式转换成void类型。

注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。它不安全,主要用于非多态的转换操作。

3 Dynamic_cast

dynamic_cast <type-id> (expression)

主要用于类层次之间的转换。

该运算符把expression转换成type-id类型的对象。Type-id 必须是类的指针、类的引用或者void*

如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么 expression 也必须是一个引用。

dynamic_cast运算符可以在执行期决定真正的类型。如果 downcast 是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 downcast 不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。而static_cast不允许,会编译错误。(比如类A是基类,类B继承自类A,类C继承自类A,但类B和类C是没有关系的,在B和C之间转换使用static_cast会发生编译错误,而dynamic_cast会返回空指针。)

在类层次间进行上行转换时,dynamic_caststatic_cast的效果是一样的;

在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。但下行转换要求基类必须有虚函数,不然dynamic_cast会编译错误。这是由于运行时需要检查类型信息,而这个信息存储在类的虚函数表,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。

想想结果为什么不是I am a grand class!

#include <cstdio>
#include <iostream>

using namespace std;

class Grand
{
private:
    int hold;
public:
    Grand(int h=0):hold(h){}
    virtual void speak() const {cout<<"I am a grand class!\n";}
    virtual int Value() const {return hold;}
};

class Superb:public Grand
{
public:

    Superb(int h=0):Grand(h){}
    void speak() const {cout<<"I am a super class!\n";}
    virtual void say() const{cout<<"I hold the super value of "<<Value()<<" !\n";}
};


int main()
{
    Grand *pg=new Grand(10);
    Superb *ps;

    //下行转换
    //if(ps=static_cast<Superb*>(pg))   //本来没有被注释掉,但出错了
       // ps->say();

    if(ps=dynamic_cast<Superb*>(pg))
        ps->say();

    //上行转换
    Superb *ps2=new Superb(20);
    Grand *pg2;

    if(pg2=static_cast<Grand *>(ps2))
        pg2->speak();

    if(pg2=dynamic_cast<Grand *>(ps2))
        pg2->speak();

    return 0;

}

4 Reinterpret_cast

不到万不得已,不要使用这个转换符,高危操作。使用特点:

 a 从底层对数据进行重新解释,依赖具体的平台,可移植性差;

 b 可以将整形转 换为指针,也可以把指针转换为数组;

 c 可以在指针和引用之间进行肆无忌惮的 转换。

reinterpret_cast<type-id> (expression)

type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。

操作符修改了操作数类型,但仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换

所谓“通常为操作数的位模式提供较低层的重新解释”也就是说将数据以二进制存在形式的重新解释。

reinterpret_cast是为了映射到一个完全不同类型的意思,这个关键词在我们需要把类型映射回原有类型时用到它。我们映射到的类型仅仅是为了故弄玄虚和其他目的,这是所有映射中最危险的。

Reinterpret_cast不支持所有的类型转换。例如:

(1)可以将指针类型转换为足以存储指针表示的整形。但不可以把指针转换为更小的整形或浮点型。

(2)不可以把函数指针转换为数据指针,反之亦然。

static_cast和reinterpret_cast的区别主要在于多重继承

Static/Const关键字

不在类中的static

static主要有3条作用

1)隐藏

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

当加了static,这些变量和函数就只在本源文件可见,对其他源文件隐藏。(可以利用这一特性在不同的文件中定义同名函数和同名变量,而不必担心命名冲突)。

2)默认初始化为0,包括未初始化的全局静态变量和局部静态变量。

未初始化的全局变量也都有这一个特性。因为未初始化的全局变量和静态变量是存储在同一个区域中(BSS段)。

在BSS段中,所有字节默认为0x00,这在某些时候可以减少工作量。例如要定义一个稀疏矩阵,如果定义为静态的或全局的,就省去了置0的操作。

sa和sa2元素类型是string,自动调用string类的默认构造函数将元素初始化为空字符串。

ia为全局静态数组,自动置0

Ia2在函数体内部定义,值不确定。

3)保持局部变量内容的持久。

static局部变量可以存放在bss段或数据段中(未初始化就存储在bss中,否则存储在数据段中)。

类中static的作用

表示属于一个类但不属于此类的任意特定对象的变量和函数。static成员可以是函数或数据,都独立于类对象而存在。

1)静态数据成员

静态成员是隶属于类的,只有一份,被每个对象共享,可以直接用类名调用,也可以被类对象调用,但通过类对象调用依然是共享的数据成员。

2)静态成员函数

Const

C++中,const限定符把一个对象转换为一个常量。因为常量在定义后不可以修改,所以定义时必须初始化。

C/C++const的区别

const代替#define的值替换功能

#define BUFSIZE 100(C)

Const int bufsize=100;(C++)

BUFSIZE是一个名字,不占用存储空间且只可以放在一个头文件中,目的是为使用它的所有编译单元提供一个值。

指向const的指针和const指针

可以使用下列方式中任意一个把一个const指针变为指向一个const对象

Double d=1;

Const double *const x=&d;

Double const *const x=&d;

即,现在指针和对象都不可以变。

Const修饰返回值

存储在栈和常量区

Const修饰函数参数

如果函数是以值传递的,可用const限定函数参数。告诉编译器i的值在函数体内不会也无法改变。由于是传值,这意义不大。

但如是传递地址,无论什么时候传递一个地址给一个函数,都应该用const修饰它(除非确实需要在函数体内部修改他),如果不这样,那么指向const的指针无法做实参。

const在类中的应用

const成员函数

const数据成员

STL序列式容器

其中的元素可序,但未必有序,C++本身提供了一个array,STL还提供了vector list slist deque ,heap priority_queue,stack,queue。

heap的底层实现是vector,priority_queue的底层实现是heap.

stack和queue是把deque改变端口之后生成的额,被称为配接器。

   Vector

vector与array的数据安排和操作方式很相似。但array是静态空间,一旦配置就不可以更改。vector当备用空间不足时,就会重新分配是当前空间二倍大小的空间,把原始数据拷贝至新址,再释放原来空间。即“配置新空间,数据移动,释放旧空间”。

vector的迭代器

vector维护的是一个连续线性空间,普通的指针就可以作为vector的迭代器而满足所有必要条件,vector的迭代器即普通指针。

vector基本操作

Begin()  返回第一个元素迭代器

End() 返回最后一个元素的下一个迭代器

Size() 返回目前容器的元素个数

Capacity()  返回目前容器的最大容量

Empty()  布尔值,判断容器是否为空

支持以数组下标方式随机访问

Front()  返回第一个元素

Back()  返回最后一个元素

Push_back(x)  把元素x放到容器最后的位置

Pop_back() 把容器最后一个元素清除

Erase(iterator it)  清除指定位置的元素,返回下一个迭代器位置

Erase(iterator it1,iterator it2) 会清楚[it1,it2)区间的所有元素

Clear()   把容器清空

Find(iterator it1,iterator it2,int num) 在区间[it1,it2)之间寻找数据num,如果找到返回迭代器位置,否则返回end()

Insert(iterator it1,n,num)在迭代器it1前方插入n个num

Insert(iterator it1,num)在迭代器it1前方插入num,返回指向num元素的迭代器

迭代器失效的几种情况:

(我理解的迭代器失效就是之前指针指向的元素发生了变化)

(1)vector的push_back()操作可能会引起空间重新配置,那么原来的迭代器将会全部失效,比较明智的做法是,每一次push_back()之后都重新确定迭代器。即vector的push_back()操作可能会引起迭代器的失效,要么只是end()迭代器失效(备用空间足够),要么全都失效(备用空间不足,已经分配了新空间)

(2)Vector的erase操作也会引起迭代器失效,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。

(3)insert操作,当没有备用空间时,会全部失效,因为空间发生了重新配置。如果备用空间充足的话,插入位置以及之后的迭代器都会失效。

int main()
{
    vector<int> vi;
    for(int i=1;i<=10;i++)
    {
        vi.push_back(i);
    }


    cout<<"测试插入失效"<<endl;
    vector<int>::iterator it;
    it=vi.begin();
    it++;  //指向第二个元素
    vi.insert(it,0);    //返回指向插入的元素本身的迭代器,it之前指向2,现在指向0
    cout<<*it<<endl;
    cout<<(*it)--<<endl;
    cout<<*it<<endl;
    cout<<*(++it)<<endl<<endl;


    //解决办法,插入之后,把迭代器后移
    it=vi.end();
    it--;   //指向10
    cout<<*it<<endl;
    vi.insert(it,99);
    it++;
    cout<<(*it)+1<<endl;

    return 0;
}

发现一个有趣的东西

答案都是12。后来问大佬,提示vector只有3个指针,start,finish和end_of_storage。即4*3=12

vector的初始的扩容方式代价太大,初始扩容效率低, 需要频繁增长,不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用resize()和reserve()方法提高效率减少内存碎片的。

resize():

void resize (size_type n);

void resize (size_type n, value_type val);

1、resize方法被用来改变vector中元素的数量,我们可以说,resize方法改变了容器的大小,且创建了容器中的对象;

2、如果resize中所指定的n小于vector中当前的元素数量,则会删除vector中多于n的元素,使vector得大小变为n;

3、如果所指定的n大于vector中当前的元素数量,则会在vector当前的尾部插入适量的元素,使得vector的大小变为n,在这里,如果为resize方法指定了第二个参数,则会把后插入的元素值初始化为该指定值,如果没有为resize指定第二个参数,则用默认值填充新位置,一般为0;

4、如果resize所指定的n不仅大于vector中当前的元素数量,还大于vector当前的capacity容量值时,则会自动为vector重新分配存储空间;

reserve():避免了频繁的申请内存空间,造成过多内存碎片

void reserve (size_type n);

1、reserve的作用是更改vector的容量,使vector至少可以容纳n个元素。

2、如果n大于vector当前的容量,reserve会对vector进行扩容。其他情况下都不会重新分配vector的存储空间

3、reserve方法对于vector元素大小没有任何影响,不创建对象。

vector中数据的随机存取效率很高,O(1)的时间的复杂度,但是在vector 中随机插入元素,需要移动的元素数量较多,效率比较低。

resize和reserve的区别:初探STL:vector中resize和reverse的区别_线上幽灵-CSDN博客

list

相较于vector的连续线性空间,list复杂得多,但好处是每次删除或插入一个元素,就配置或释放一个元素空间,因此,它对于空间的运用非常精准,一点也不浪费,对任意位置的插入和删除,永远都是常数时间。

list本身是一个双向链表。还是一个环状双向链表,只需要一个指针便可以完整表现整个链表。让节点指针指向刻意位于尾端的一个空白节点,便可以实现前闭后开的特性。

list迭代器

不像vector那样以普通指针作为迭代器,因为节点不保证在空间连续存在。

迭代器包括3个部分:指向下一个节点的指针/指向上一个节点的指针/数据元素。

list的插入操作(insert)和接合操作(splice)都不会造成原有迭代器失效,甚至list的元素删除操作也只会使得指向被删除元素的迭代器失效,其他迭代器不受影响。

list的基本操作

Begin()

End()

Empty()

Size()

Front()

Back()

Find(it1,it2,num)返回第一次出现Num的迭代器

Push_back(x)

Insert(it1,x);

Erase(it1);

Erase(it1,it2);

Push_front(x);

Pop_back();

Pop_front();

Clear();  恢复初始状态即空链表  node->next=node;node->pre=node;

Remove(x);将值为x的元素全部移除

Unique():移除数值相同且连续的元素,最后保留一个。

内部实现:

//Splice(it1,list2)把整个list2链表都插入到it1处所指的迭代器。List2必须不同于调用它的list

//Splice(it1,it2,It3)把[it2,it3)范围的元素接合到it1处,可以是同一个list

关于splice函数:list::splice函数的使用_Rushinger-CSDN博客

void list<T,Alloc>::unique()
{

    iterator first=begin();
    iterator last=end();


    if(first==last)   //空链表,什么也不做
        return;

    iterator next=first;
    while(++next!=last)   //遍历每一个节点
    {
        if(*first==*next)   //如果有相同的元素就移除
                erase(next);
        else
            first=next; 
        next=first;
    }
}

Merge(list2)把List2与调用它的List合并,2个list必须已排序

Reverse(),把调用它的链表反转

Sort()排序,不可以使用STL的sort,因为那个是随机存储迭代器。使用面向对象的方法调用,即li1.sort(cmp),cmp是比较函数。

deque

Deque

vector是单向开口的连续线性空间,deque则是一种双向开口(在头尾2端都可以做元素的插入和删除操作)的连续线性空间。虽然vector也可以在头尾2端进行操纵,但是其头部操作效率极差,无法接受。

deque与vector的最大差别在于

(1)deque允许常数时间内对头端进行元素的插入和删除操作。

(2)deque没有所谓容量的概念,它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。不必像vector那样因容量不足,重新分配更大的空间,然后复制元素,释放原空间。

deque的迭代器不是普通指针,比vector的迭代器复杂得多,除非必要,应尽量使用Vector而不是deque。比如对deque的排序操作,为了效率,可以先将deque完整复制到一个vector中,将vector排序后,再复制回deque中。

deque的中控器map

deque是由一段一段的定量连续空间组成,一旦有必要在deque的前端或尾端增加新空间,就会配置一段定量连续空间,串接在deque的头部和尾部。deque的最大任务就是在这些分段的连续空间上维护其整体连续的假象,并提供随机访问的接口,避免“重新配置,复制,释放”的过程,代价是复杂迭代器结构。

deque采用一小块连续的空间map(不是STL容器map)作为主控,其中每个元素(节点)都是指针,指向另一段较大的连续线性空间,称为缓冲区,缓冲区是deque的存储空间实体(缓冲区默认大小是512B)。map其实是一个二级指针,指向一个指针数组,其中每一个元素(节点)都是指向deque存储实体的指针。

一旦map所提供的节点不足,就必须重新配置一块更大的map。

(1) map尾端的节点备用空间不足

(2)map前端的节点备用空间不足

这时deque会配置一个新的map空间,把原来的map内容拷贝过来,释放map,重新设定map的起始地址和大小,重新设定迭代器start和finish迭代器。

deque的迭代器

deque是分段连续空间,维持其整体连续假象的任务,落在了迭代器的operator++和operator--身上

deque的迭代器必须可以指出分段连续空间(缓冲区)在哪里,还需要判断自己是否处在其缓冲区的边缘,如果是,一旦前进或后退就需要跳跃至下一个或上一个缓冲区。

deque的迭代器简单来说包含4个元素:

(1)Cur:   空间中当前元素的地址,是一个指针。

(2)First:  当前元素所在缓冲区的开始地址,也是一个指针。

(3)Last:   当前元素所在缓冲区的结束地址(不是结束地址的下一个地址),也是指针。

(4)Node : 当前元素所在缓冲区在map中的位置,是一个二级指针。

迭代器对各种指针运算都进行了重载操作,所以迭代器的加减,前进后退都不可以与指针相比,需要注意:一旦行进时遇到缓冲区边缘,要特别当心,可能会跳到下一个缓冲区。

deque有2个特殊的迭代器start和finish,分别指向第一个缓冲区的第一个元素和最后一个缓冲区的最后一个元素的下一个位置。

deque的基本操作

Size()

下标随机访问

Push_back(x)   当尾端只剩下一个备用空间时,会先配置一个新的缓冲区,再设置新元素内容,最后更改迭代器finish状态。

Push_front(x) 当第一缓冲区没有备用空间时,会先配置一个新的缓冲区,再设置新元素内容,最后更改迭代器start状态。

Pop_back()  当最后一个缓冲区只有1个元素时,会释放缓冲区,调整迭代器finish。

Pop_front()  当第一个缓冲区只有1个元素时,会释放缓冲区,调整迭代器start。

Find(it1,it2,num) 返回第一个值为num的迭代器

Begin()

End()    返回迭代器

Front()

Back()     返回元素

Empty()

Clear()   deque的最初状态(无任何元素时)保有一个缓冲区,clear()之后会恢复初始状态,同样保留一个缓冲区。因此会释放缓冲区内存,保留一个缓冲区,更改迭代器finish。

Erase(it) 

(1)如果清除点之前的元素个数比较少,就移动清除点之前的元素,移动之后,去除第一个元素,调整strat迭代器

(2)如果清除点之后的元素个数比较少,就移动清除点之后的元素,移动之后,去除最后一个元素,调整finish迭代器

Erase(it1,it2) 清除[it1,it2)区间所有元素

(1)如果清除区间是整个deque,直接调用clear(),调整finish迭代器

(2)如果清除区间的前方元素个数比较少,向后移动前方元素,释放前方冗余的缓冲区,调整start迭代器

(3)如果清除区间的后方元素个数比较少,向前移动后方元素,释放后方冗余的缓冲区,调整finish迭代器

Insert(it,num)

(1)插入的是最前端,交给push_front(num)

(2)插入的是最尾端,交给push_back(num)

(3)插入点之前的元素个数较少,在最前端插入第一个元素的值,移动插入点之前的元素,将值覆盖掉插入点的值。

(4)插入点之后的元素个数较少,在最尾端插入最后一个元素的值,移动元素,插入元素即可。

  satck

stack是一个先进后出的数据结构,只有一个出口,除了最顶端之外,没有任何其他方法可以存取stack元素,stack不允许有遍历行为。进栈即push,出栈即pop。

默认情况下,stack是以deque作为底层数据结构,并封闭其头端开口。stack以底部容器deque完成所有操作,称为容器配接器。

stack没有迭代器。

stack常用函数

Empty()

Size()

Top()

Push(x)

Pop()

stack也可以使用list作为其底层结构,list也是一种双向开口的数据结构。

由于不是默认,因此定义方式为:

Stack<int,list<int> > istack;

为什么stack默认不是使用vector或者list作为底层结构?

stack主要操作是push,pop,top等,理论上所有序列式容器(vector,deque,list)都可以实现。

(1)vector要实现push操作,需要调用自己的push_back,而这个操作很有可能会使容器重新配置空间大小,拷贝数据,释放原有空间。而且当实现pop操作时,vector会调用pop_back,然而vector不会缩减已经申请的空间容量,可能会造成空间浪费。

(2)用list实现时,不用考虑空间容量的变化,虽然复杂度很平稳,但每次进出栈的开销比较大。

(3)使用deque时,容器的大小发生改变所付出的代价比vector要低,并且当容器大变小时,还可以适当减小容量。

选用折中的选择deque。

queue

queue是一种先进先出的数据结构,不允许有遍历行为,只可以在2端进行入队出队操作。

queue没有迭代器,不提供遍历功能。

queue也是一种容器配接器,底层实现为deque,封闭了它底端的出口和前端的入口。

queue常用函数

Empty()

Size()

Front()

Back()

Push(x)

Pop()

queue也可以使用list作为底层容器

Queue<int,list<int>>  iqueue;

heap

heap其实不属于STL容器组件,主要为了实现priority_queue(允许用户以任意次序将任意元素推入容器,但取出时一定是从优先权最高的元素开始取,大顶堆即可满足)。

为什么不适用list

list元素插入时可以享受常数时间,但要找极值,却需要对list做线性扫描。

如果让元素插入时总是使list有序,那么元素的插入需要线性时间,虽然寻找极值需要常数时间。

堆其实是一颗完全二叉树,整棵树除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右不得有空隙。

堆可以用数组来存储所有节点,假设a[0]是无意义的值,那么当堆中的某个节点位于下标i处时,其左孩子位于2*i处,其父节点位于i/2处。

堆往往需要插入元素,需要动态改变大小,显然简单的数组无法满足,因此堆的底层实现是vector。

堆分为大顶堆/小顶堆,STL中默认使用大顶堆。大顶堆的最大值在根节点,位于底层vector的起始处。

Heap算法

Push_heap(it1,it2)

接受2个迭代器,用于表示heap底层容器的头尾,注意:这时新元素已经插入到了底层容器的尾端。如果不符合这2个条件,执行的结果未知。(新加入的元素放在vector的end()处,push_heap算法就是调整整个新区间为一个大顶堆)

Pop_heap(it1,it2)

接受2个迭代器,用于表示heap底层容器的头尾。堆的最大值在根节点,pop操作取出根节点,其实就是把这个节点的值与vector尾端节点的值交换,然后再调整堆为大顶堆(堆的大小减1).pop后,最大元素只是被置于底部容器的最尾端,未被取走,可以使用back()查看元素,使用pop_back剔除元素。

Sort_heap(it1,it2)

使用pop_heap操作,每次将最大值放到尾端,然后将操作范围减小一个元素,最后得到一个递增序列。

Make_heap(it1,it2)

用于将一段现有数据转化为heap。

heap没有迭代器

priority_queue

Priority_queue是一个queue,缺省下以vector作为底部容器,是一种容器配接器。

常用函数

Empty()

Size()

Top()

Push()

Pop()

STLsort的实现机制

这个算法接受2个随机存储迭代器,然后将区间中的所有元素以渐增方式由小到大重新排列。当然还有另外一个版本,用户可以指定一个仿函数作为排序标准。

STL中所有关系型容器都有自动排序功能(底层结构采用RB-tree)所以不需要使用sort算法。而序列式容器中的stack/queue/priority_queue都有特别的出入口,不允许用户对元素排序。剩下的vector/deque/list前2者迭代器属于随机存储迭代器,适于使用sort算法,list迭代器属于双向迭代器,slist其迭代器属于前向迭代器,这2个都不适用sort算法,但是可以使用它们自己的成员函数sort()。

sort的实现机制:

(1)数据量大时采用quicksort,即分段递归排序。

(2)分段后数据量小于某个门槛时,为避免quicksort的递归调用带来过大的额外负荷,改用插入排序。门槛值为5-20。因为插入排序在面对几近排序的序列时有很好的表现。

(3)如果递归层次过深,会改为堆排序。

具体的:

首先调用快排,并设置好分割层数。

如果区间的长度大于某一阈值,再判断分割的层数是否到达某一阈值,如果到达,采用堆排序,否则继续分割进行递归调用。

如果区间的长度小于某一阈值,转为快排。

关于分割层数:

设元素个数为n,求2^k<=n k的最大值,那么分割层数为2*k

为什么只适用于随机存储迭代器:

因为快排时的枢纽值的选取是采用三数取中法的,为了可以快速取出中央位置的元素,迭代器必须可以随机定位,必须是个随机存储迭代器。

STL中的sort  swap  find与容器方法中的sort/swap/find的区别

STL中find函数

循环查找[first,last)内的所有元素,找出第一个匹配“等同条件”者,如果找到就返回一个InputIterator指向该元素,否则返回迭代器last。

template<class InputIterator,class T>

InputIterator find(InputIterator first,InputIterator last,const T& value)

{

    while(first!=last&&*first!=value)

        ++first;

    return first;

}

关联式容器大都以RB_tree作为底层数据结构,所以它的find函数是二分查找的过程,比STL中的find函数效率高

STL中swap函数

用于交换2个对象的内容

template<class T>

inline void swap(T& a,T& b)

{

    T tmp a;

    a=b;

    b=tmp;

}

list中的sort成员函数使用quicksort

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值