C++快速复习

1. 基本结构
    C++语言由头文件与源文件组成。
    头文件中包含:版权与版本信息,宏定义,函数声明和类的声明。宏定义为是了防止头文件被重复引用。<>引用头文件则直接在系统库中查找头文件;“ ”引用头文件则先在当前工程目录下查找,若工程目录中不存在然后在系统库中查找。
    源文件中包含:版权与版本信息,对头文件的引用,系统功能代码的实现。用ifndef/define/endif结构产生预处理的宏定义块。
    类成员函数可以在类声明中被定义,并且自动生成内联函数,这虽然会使书写方便,但会造成风格上的不一致,因此建议头文件中只存放声明而不定义。
    头文件的作用:1.很多时候不便公开头文件代码,用头文件来调用库功能,编译器从库中提取相应的代码;2.头文件能加强安全类型的检查,如果接口实现错误会报错,以便开发者调试。

2.C++基本要素
    标识符:只能是字母、数字、下划线组成,且不能以数字开头。标识符在C++中区分大小写,长度是任意的,一般前1024个是有意义的。C语言中一般前16个是有效的。

    常量:在运行时不能被改变,在定义常量时可以设置初始值。对于常量,编译器将其放置于一个只读的存储区域。

    变量:在程序中可以被随意赋值,对于每一个变量都有两个属性:左值和右值。左值是指变量的地址值,即存储变量的内存地址;右值是变量的数据值,即内存地址中存储的数据。

    变量初始化的几种形式:

3.变量的存储类型
    变量的声明是告知编译器变量的名称和数据类型,变量的定义是为变量分配存储区域。使用extern只声明而不定义变量;若一个文件中声明定义一个全局变量int var=0; 在另一个文件中声明extern int var; 即可访问此变量;

    Static存储类型表示变量在函数或文件内的“持久性”,也称为静态变量,静态变量可分为局部静态变量和全局静态变量。

    局部静态变量:函数内的变量用static修饰时,将被分配在持久的存储区域,当函数调用结束后并不释放,保留其值以便下次调用。局部静态变量的作用域为当前函数,不能被外界函数和文件访问;

    全局静态变量:作用域仅限于当前定义的文件,不能被其它文件使用extern关键字访问;

    使用register关键字,表示变量将被放置在CPU寄存器中,访问register变量要比访问普通变量快得多,但register只能用于局部变量或作为函数的形式参数,不能用来定义全局变量。

    变量有静态存储和动态存储,全局变量和静态变量是静态存储的,普通的局部变量是动态存储的,auto关键字表示变量动态存储,默认情况下,局部变量均属于auto变量。

4.数据类型
    数值类型分为整型和实型,整型又分为有符号型和无符号型。

    两个实数比较,尽量不要使用“==”或“!=”,而要使用“>=”或“<=”之类的运算符。因为用等于或不等于时,若程序精度要求较高,可能产生未知的错误,也可能产生未知的结果。通常在比较实数时要定义实数的精度,在这个精度内来确定两数的等或不等。

    字符类型:C++中用单引号来确定字符变量,用双引号来确定字符串变量,字符是以ASCII的形式存储的,因此可以直接将整数赋值给字符变量。字符存储用一个字节。

    数组类型:数组的初始化要注意一些细节,如下图。定义二维数组时,可以省略第一维的长度,但不可以省略第二维的长度。

    布尔类型:布尔类型和整数类型可以相互赋值。

    枚举类型:枚举类型是用int类型实现的,占用4个字节,定义格式为:enum<枚举类型名>{常量1,常量2,…},定义枚举类型时可以为各常量提供一个整数值,默认情况第一个数为0,没有定义的值应为前一个值加1。在定义函数时将函数参数设置为枚举类型,这样可以限制调用函数必须提供枚举类型中的某个常量,而不能随意输入一个整数。

    结构体类型:定义格式为struct<结构体类型名>{结构体类型变量};  若只要定义一次结构体类型变量,可以不写结构体类型名称。访问结构体成员用“.”,两个结构体变量可以直接赋值。

    结构体空间分配问题:涉及字节对齐问题,即编译器在为结构体变量分配空间时,保证下一个成员的偏移量应为该成员数据类型长度的整数倍。在开发应用程序时,有时要用一个字节表示多项内容,这时就要用位域来访问一位数据,如下代码所示:

    共用体提供了一种机制,使多个变量(共用体中的成员)可以共享同一个内存地址,各成员内存起始地址位置相同,每一瞬间只有一个成员起作用,起作用的成员是最后一次存放的成员。

    指针是用来存放变量地址的。通过变量访问变量是直接访问,通过指针访问是间接访问。注意区分指针数组和数组指针。如下,

    用const关键字来修饰指针的几种情况:

    引用即是目标的一个别名,操作引用与操作实际的目标对象是相同的。引用的定义格式如下:数据类型&引用名称=目标对象。

    用typedef关键字可以自定义数据类型,它不是创建一个新的数据类型,而是为已有数据类型创建一个新的名称,使用自定义类型可以提高程序的移植性。语法格式为:typedef 数据类型 新名称

5.运算符
    异或有很强大的功能,通常用异或运算来实现二进位的反转,也可用来实现两个数的互换。

    任何数与0进行按位异或运算,结果为数据本身;

    变量与自身按位异或运算,结果为0;

    按位异或运算具有交换性,a^b^c=a^c^b=b^a^c;

    用异或运算来交换两个数的例子如下:

Sizeof()用于返回变量、对象或数据类型的字节长度,在32位系统中,指针的长度为4字节。
 

 

1.栈存储与堆存储的区别
    C++程序编译占用的内存可以分为以下几个部分:
    栈区:由编译器自动分配释放,存放函数的参数值、局部变量等,用于存放占用空间小、生命周期短的数据;

    堆区:由用户分配释放,用于存储占用空间大、生命周期长的数据,如静态变量和全局变量。手动分配空间后一定要释放空间,否则会导致内存泄漏;

    全局区:全局变量和静态变量是存储于在一起的,初始化的存储一块区域,未初始化的存储相邻区域;

    文字常量区:常量字符串存放于此区,程序结束后由系统释放;

    程序代码区:存放函数体的二进制代码;

堆与栈的区别可以分为三个方面:

堆栈在空间上的区别见上面;
堆栈在缓存方式上的区别:栈使用的是一级缓存,通常都是被调用时处于存储空间,调用完成立即释放;堆是存放在二级缓存中的,生命周期由虚拟机的垃圾回收算法来决定,故调用这些对象的速度相对较慢;
堆栈在数据结构上的区别:堆数据结构可以看成是一棵树,如堆排序;栈数据结构是一种先进后出的数据结构;


逗号表达式:它的运算优先级最低,逗号表达式取最右边表达式的值。

const与define的比较:

    C++语言可以使用这两者来定义常量,但是使用const更好,理由如下:

const常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行安全检查,而对后者只能进行字符替换,字符替换可能会发生意想不到的错误;
有些集成化调试工具可以对const常量进行调试,但不能对宏常量调试,此时只能使用const定义常量。更精确地说,const在程序运行时只有一份复制品,而define有多份复制品。
2.类中的常量问题
    在类中定义常量,我们很自然地想到使用const,但是const数据成员只对某个对象生存周期内是常量,而对整个类而言是可变的,因为创建多个对象时,不同对象其const数据成员的值可以不同。不能在类声明中初始化const成员,其初始化只能在类的构造函数初始化列表中进行。因为类未被创建时,编译器不知道相应的常量是什么。

    应该使用枚举常量来在类中建立一个恒定的常量,枚举常量不会占用对象的存储空间,在编译时全部求值。

3.C++相关语句
    C++语句一般由表达式和分号构成的,只有空分号的语句为空语句,程序中允许有多条空语句,空语句不执行任何功能。{}是复合语句,也可用复合语句来代替空语句,但是括号后面没有分号。

    多条if语句,程序会依次执行各条语句,从而条件判断较为复杂。对于if/else语句,程序只选择一个分支按条件执行。

    return语句用来退出当前函数的执行,若函数没有返回值,则只使用return语句,后不加任何表达式。在使用return语句时,要注意如果代码之前在堆中分配了内存,则在return语句前要释放内存,以防止内存泄漏。

    exit语句用于终止当前进程,通常用于结束当前的应用程序。exit包含一个整型参数,用于标识退出的代码,与return不同的是,return语句只退出当前调用的函数,除非当前函数是主函数,return会结束当前进程,而exit会直接结束当前进程,无论当前函数是否是应用程序的主函数。

4.C++函数相关
在定义函数时,若函数返回类型不是void类型,一定要在函数中加入return语句;
若调用函数处于被调用函数下方,则要对被调用函数进行前置声明,声明也可放置于main函数内部。
若函数有多个参数,应保证默认参数出现在参数列表的最右方。
在定义数组参数、函数参数时,也可不指定数组的大小,在调用C++编译器时不对数组长度进行检查,只是将数组首地址传给函数,但是使用数组的引用会强制检查参数中元素的个数,这可以更好地确定程序的安全。
    值传递:在函数调用时,将实际参数的值复制一份传递到调用函数中,实际参数值不受影响;

    引用传递:即传址调用,按引用传递时,调用函数修改了参数值,实际参数值也会发生改变。

    因此,通常在定义函数时,如果参数为数组、指针或引用类型,则用引用类型传递方式,否则用值传递方式。

内联函数:

    内联函数是指对于程序中出现函数调用的地方,如果是内联函数,编译器直接将函数代码复制到函数调用的地方,省去了程序跳转的过程,用inline关键字标识内联函数。内联函数的目的是提高程序的执行效率。

    对于使用inline关键字的内联函数,程序不一定将函数作为内联函数对待,这关键取决于编译器的优先机制。通常对于代码较少、频繁调用的函数,可以标识为内联函数,否则不使用内联函数。

函数的重载:

    重载是多个函数具有相同的函数名称,而参数类型或参数个数不同,在调用时编译器正是通过参数类型和参数个数来区分调用哪个函数。

    返回值不作为区分重载的一部分;对于参数来说,const关键字不作为区分,但若参数是指针或引用类型,const作为区分标志;参数默认值不作为区分标志;

注意:在局部域中定义声明函数,将使同名函数隐藏而不是重载,要访问其他域函数,用::域访问符。

函数指针:

    在C++语言中,函数名实际上是指向函数的指针,定义有如下几种形式:

    对于函数指针来说,它指向的函数必须与函数指针定义的形式相同,即返回类型、参数、参数个数均相同。在程序中使用函数指针可以极大的丰富程序的灵活性。

5.局部作用域和全局作用域
    局部作用域描述的是函数体中变量、常量等对象的作用范围,对于处于同一个作用域的对象来说,对象不允许重名。当编译器在当前代码处发现变量名时,它将在当前局部作用域中搜索变量的定义,如果未定义则向外搜索,直到局部作用域完成;

    全局作用域是函数、变量、常量对象作用范围是整个应用程序。对于全局变量,若未初始化,其存储区为0,对于局部变量,若未初始化,则其值是不可预见的。

6.定义和使用命名空间
    在一个应用程序中的多个文件可能存在同名的全局对象,这样会导致应用程序的链接错误,使用命名空间是消除命名冲突的最佳方式。命名空间的定义格式如下:

    若要使用命名空间中的对象,则要在对象前加上命名空间的名字,若要访问命名空间的多个对象,可以使用using命令,using的作用域是从当前引用处到当前作用域结束。

    对于同一个命名空间,可以在多个文件中定义,此时命名空间中的内容是各个文件中内容之和。命名空间也可嵌套,访问内层函数时,可以用using namespace 外层名称::内层名称。

    using命令用于引用命名空间中的全部空间对象,容易和正在编写的程序中的局部变量发生冲突,导致命名空间的变量被隐藏,故不建议过多使用此命令。不命名的空间中的对象只适用于当前文件,各文件中不能相互访问,在一个文件中访问空间中的对象与访问普通全局对象是相同的。

7.函数模板
函数模板提供了一种机制,使函数返回值、参数类型能够被参数化,而函数体保持不变,这极大的增强了函数的灵活性。定义格式如下:

函数模板也可以重载,要注意区分。
 

 

面向对象的基本任务是描述对象并对对象进行归类总结。类类型与int类型一样,也没有任何内存分配。类的属性和对外接口是类定义的重点和难点,原则是尽量让内部操作私有化,提供简单易用的接口函数。

1、类的相关问题
在定义类的数据成员时,不能像定义变量一样进行初始化,若在定义时未指明访问限定符,默认为private;

在定义类的方法时,若方法中不用修改类的数据成员,则最好在方法声明的最后使用const关键字,表示用户不能在此方法中修改类的数据成员。若类中包含指针成员,在const方法中不可以重新为指针赋值,但可以修改指针所指向的地址中的数据。

类中,除静态成员可以直接访问外,其它成员是通过对象来实现访问的。在定义类时并没有分配存储空间,只有当实例化对象时,才分配存储空间。也可以将类对象声明为一个指针,并使用  new运算符为其分配内存,如下所示:

若将类对象声明为常量指针,则只能调用类中的常量方法。

2、构造函数与析构函数
每个类都有构造函数与析构函数,构造函数在定义对象时被调用,析构函数在对象释放时被调用。构造函数负责类对象生成之前的初始化,析构函数负责对象销毁后的处理。

构造函数没有返回值,若用户没有提供构造函数和析构函数,则系统使用默认的构造析构函数。一个类可以包含多个构造函数,各函数通过重载来进行区分,下面是指针调用参数构造函数进行初始化:

类体中定义的数据成员不能初始化,故类中初始化只能借助构造函数的初始化列表实现。类成员函数,若在类体中定义,函数前即使没有使用inline,该成员函数也被认为是内联函数。

析构函数没有返回值,也没有参数,故不能重载。

3、复制构造函数
复制构造函数与类的其它函数构造类似,以类名作为函数的名称,第一个参数为该类的常量引用类型。

下面的三种情况要用到复制构造函数:

对象以值传递的方式传入函数参数;

对象以值传递的方式从函数返回;

对象需要通过另外一个对象进行初始化;

编译器有默认的拷贝构造函数,这个函数仅仅是使用老对象的数据成员的值对“新对象”数据成员一一进行赋值,这称为浅拷贝,即只是对象中数据成员进行简单的赋值,这种方式在对象中存在动态成员时则会出现问题,比如对象中的指针。浅拷贝只是使两个指针有相同的值,而不是我们需要的两块不同的地址。

深拷贝,对于对象中动态成员,不仅仅赋值,还重新动态分配空间,从而使得指针指向两块不同的内存,但内存中的值相同。

在编写函数时,尽量按引用方式传递参数,这样可以避免调用复制构造函数,可以极大地提高程序效率。也可以将拷贝函数放在private中,从而防止默认拷贝的发生。

4、静态类成员
普通类成员只能通过实例化对象访问,静态类成员还可以通过类名直接访问,访问时用::域访问符。在定义静态数据成员时,要在类体外部对静态数据成员初始化。静态数据成员是被所有类对象共享的。

静态数据成员可以是当前的类型,而其他数据成员只能是当前类的指针或引用类型,如:

针对静态数据成员有如下几点:

静态数据成员可以作为成员函数的默认参数,但是普通成员不可以;

类的静态成员函数只能访问类的静态成员,不能访问普通数据成员;

静态成员函数末尾不能用const关键字修饰;

静态数据成员和静态成员函数在类体之外初始化或定义时,去掉static关键字;

5、运算符重载
运算符重载要用到operator关键字,它其实是函数重载的一种,因为运算符本来就是一个函数。对于重载的运算符,两个函数不能交换顺序,重载是什么顺序,只能用这各顺序调用。

对于++和--运算符,由于涉及前置和后置,故默认情况下,重载运算符没有参数,表示是前置运算;若用整型int作为参数,则表示后置运算。

并不是所有的运算符都可以重载,大多数是可以重载的,但是::,?,:,. 是不能重载的。

6、有关类的sizeof问题
空类也会被实例化,编译器会给类隐含添加一个字节,故空类的sizeof()结果为1;sizeof()用来计算字符串的长度时包含”\0”,strlen()统计的长度不包含”\0”。

构造函数、析构函数都不归入sizeof()统计范围之内。

虚函数由于要维护在虚函数表中的位置,故要占据一个指针的大小。

静态成员也不归入sizeof()统计范围。

总起来说,类的大小与非静态成员大小和虚函数有关,与其他普通成员函数无关。类的大小也遵守内存分配时的字节对齐规则:

7、友元类与友元方法
当用户希望另一个类可以访问当前类的私有成员时,可以在当前类中将另一个类作为自己的友元类。友元即朋友,私有成员只有朋友可以访问。

若只想让某个成员函数访问类的私有成员,则可以将此函数声明为类的友元函数,即在函数返回值前加上friend关键字。友元函数不仅可以是类成员函数,也可以是全局变量函数。

8、引用和指针的区别
引用是一个变量的别名,引用被创建的同时必须被初始化,指针可以在任何时候被初始化;

不能有NULL引用,引用必须与合法的存储单元关联,指针则可以是NULL;

引用一旦被初始化,就不能改变引用关系,指针则可以随时改变所指的对象。

9、类的继承
继承是面向对象的主要特征之一,它使一个类可以从现有类中派生,而不必重新定义一个新类,类继承时使用“:”运算符。

类继承与访问关系如下表:

用public继承时,原类中public和protected在派生类中类型不变;

用protected继承时,原类中public和protected在派生类中都变成protected类型;

用private继承时,原类中public和protected在派生类中都变为private类型;

用户在父类派生子类时,可能存在一种情况,即在子类中定义了一个与父类同名的方法,此时称子类隐藏父类方法,这样父类中所有的同名方法包括重载方法均被隐藏。若要访问父类中的方法,可用域访问的方式。

在派生完一个子类后,可以定义一个父类指针,通过子类构造函数为其创建对象。因为编译器对同名方法是静态绑定的,即根据对象定义的类型来确定调用哪个类方法。

在定义方法时,在方法的前面使用virtual关键字,这种是虚方法,使用虚方法可以实现动态绑定,即根据对象运行的类型来确定调用哪个类的方法。在父类中定义的虚方法,在子类中同名的方法,即使前面没有virtual关键字,也为虚方法。

纯虚方法:

C++中除了能定义虚方法外,还可定义纯虚方法,也即抽象方法。一个包含纯虚方法的类称为抽象类。抽象类不能实例化,不能当返回值,不能做函数参数,但可以设置指针,它常用于接口的实现。

抽象类常作为其它类的父类,从抽象类派生的子类要实现父类中所有的纯虚方法。定义纯虚函数的意义在于告诉子类,这个接口是一定要继承的,并且要重写函数体,增强它的功能。

10、子类对象的创建和释放
当从父类派生一个子类,定义一个子类对象时,它将依次调用父类构造函数、子类构造函数,在释放对象时,先调用子类析构函数,再调用父类析构函数;

当定义一个父类对象,调用子类创建一个对象时,它依次调用父类构造函数、子类构造函数,在释放时分两种情况:

1.析构函数不是虚函数,则只调用父类析构函数;

2.析构函数是虚函数(父类),则先调用子类的析构函数,再调用父类析构函数。

由此可见,若父类不是虚析构函数,在子类分配了空间,没有调用了类的析构函数会产生内存泄漏,因此在编写函数时,析构函数常用虚函数。

11、多继承
多继承是子类可以继承多个父类,各父类间用逗号隔开,都要带关键字。若不同父类中有同名方法,子类实例在访问时要指明父类的名称。多继承的继承链不能有环,可以用UML语言画出继承图。

在多继承时,通常第三代子类中存在两个父类的备份,C++提供了一种虚继承机制使之只有一个备份,方法是在两个子类继承父类时,加上virtual关键字。虚继承不再从第一代父类开始创建构造函数,直接从两个子类开始创建。

12、类模板
类模板能够为类的数据成员、成员函数的参数、返回值提供动态参数化的机制,使用户可以方便地设计出功能更为灵活的类。

类模板中也可以设置静态数据成员,不同类型的模板实例都有各自的静态数据成员,同一类型各实例共享静态数据成员。

 

 

接上篇,这篇主要归纳一下C++中的内存使用问题。

1、内存的分配方式
从静态存储区分配。这种方式在程序编译的时候已经分配好,在程序的整个运行期间均存在,如全局变量、静态变量;

从栈上创建。在执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数结束时这些单元被自动释放,栈内存分配运算内置于处理器指令集中,效率较高,但容量有限;

从堆上分配。程序运用malloc或new申请任意多少的内存,用户手动用free或delete释放。动态内存的生存周期由我们决定,使用灵活,但也容易出问题。

2、常见的内存错误及处理方法
内存未分配成功去使用它。故在malloc或new申请内存后,立即检查指使是否为NULL,防止使用指针为NULL的内存;

内存分配成功但未初始化。故不要忘记为数组或动态内存赋初值,防止未被初始化的内存作为右值使用;

分配成功也初始化但越界。故要注意不要让数组或指针下标越界;

忘记释放内存,造成泄漏。故动态内存申请与释放必须要配对;

释放了内存却继续使用它。这有三个方面:

首先,程序中对象调用关系复杂,此时应重新设计数据结构,从根本上解决对象管理方面的问题;

其次,return语句写错了,不能返回指向栈内存的指针或引用,因为该内存在函数体结束时被自动销毁;

最后,使用delete或free后,没有将指针设置为NULL,导致产生“野指针”。

3、指针和数组的对比
数组要么在静态区被创建(全局数组),要么在栈上被创建。数组名对应着一块内存,其地址和容量在生命周期内保持不变,只有内容可以改变;

指针可以随时指向任意的内存块,特征是可变,故常用指针来操作动态内存;

不能对数组名进行直接复制与比较,C++中也无法知道指针所指的内存容量,除非申请时就记住它,因此sizeof(p)=4字节;

当数组作为函数参数进行传递时,此数组自动退化为同类型的指针,故sizeof(arr)=4字节;

4、free和delete指针操作
两者只是将指针所指的内存释放掉,并没有消灭指针,故释放内存后,一定要将指针置空。注意:指针消亡了,并不表示所指的内存会被自动释放;内存释放了,并不表示指针会消亡或成为     NULL指针。

野指针不是NULL指针,而是指向垃圾内存的指针,产生野指针主要有三个方面:

指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,而是随意指的,故在创建指针变量时,要么初始化为NULL,要么让其指向合法的内存;

指针在free或delete后没有置为NULL;

指针操作超越了变量的作用范围;

5、malloc和new操作
malloc和free是C/C++标准库函数,new和delete是C++运算符。

对于内部数据类型而言,没有构造函数和析构函数,用malloc/free和new/delete是等价的;对于非内部数据类型而言,malloc/free无法满足动态内存分配的要求,要用new/delete,这是因为malloc/free是库函数,不在编译器控制权限之内。

new比malloc简单很多,是因为new内置了sizeof()、类型转换和类型安全检查功能,对于非内部对象而言,在创建动态对象的同时完成了初始化工作,若对象有多个构造函数,则new语句也会有多种形式的初始化。

6、内存耗尽如何处理
若在申请动态内存时,找不到足够大的内存块,malloc和     new将返回NULL指针,宣告内存申请失败。处理内存失败有3种方式:

判断指针是否为NULL,若是,马上用return语句终止本函数;

判断指针是否为NULL,若是,马上用exit(1)终止整个程序运行;

可以为new和malloc设置异常处理函数;
 

 

接上篇,这篇主要对C++一些高级特性进行归类总结。

1、函数重载、覆盖和隐藏
相对于C函数而言,C++增加了重载、内联、const和virtual四种机制,其中重载和内联机制可用于全局函数,也可用于类成员函数,const与virtual机制仅用于类的成员函数。

类成员函数被重载的特征:

a.    相同的范围(在同一个类中);

b.    函数名称相同;

c.    参数不同;

d.    Virtual关键字可有可无;

覆盖是派生类中函数覆盖基类函数的特征:

a.    不同范围(分别位于派生类和基类中);

b.    函数名称相同;

c.    参数相同;

d.    基类函数必须要有virtual关键字;

隐藏的特征:

a.    若派生类与基类函数同名,但参数不同,此时无论基类是否有virtual关键字,基类函数被隐藏;注意与重载区别。

b.    若派生类与基类函数同名,参数也相同,但基类没有virtual关键字,此时基类函数被隐藏;注意与覆盖相区别。

2、类的三种基本函数
构造函数、析构函数和赋值函数是每个类最基本的函数。每个类只有一个析构函数和一个赋值函数,可以有多个构造函数。对于任意一个类,若未编写各个函数,编译器会默认产生四个函数,如下所示:

注意:拷贝构造函数是在对象被创建时调用,赋值函数只能被已经存在的对象调用。

3构造函数的初始化列表
初始化列表位于函数参数表后,函数体前,故列表的初始化工作发生在函数体内代码之前。列表中各量的初始化顺序与变量在类中定义的顺序相同,与排列顺序无关。初始化列表的使用规则如下:

a.    若类存在继承关系,派生类必须在初始化列表中调用基类的构造函数;

b.    类的const常量只能在初始化列表中初始化,不能在函数体内赋值;

c.    类的数据成员可以采用初始化列表和函数体赋值两种方式。对于非内部数据类型成员对象应采用初始化列表方式,这样效率更高;对于内部数据类型,两种方式效率相同,但用赋值方式更清晰;

4、如何在派生类中实现类的基本函数
基类的构造函数、析构函数、赋值函数不能被派生类继承,派生类实现基本函数的原则如下:

派生类的构造函数应在初始化列表中调用基类的构造函数;

基类与派生类的析构函数应为虚函数;

在编写派生类的赋值函数时,要将基类的数据成员重新赋值,若基类的数据成员私有,可以调用基类的赋值函数;

当基类构造函数要传递参数才能初始化时,派生类必须显式定义构造函数,为基类传参数;

5、用const来提高函数的健壮性
const不仅定义常量,更大的作用是可以修饰函数的参数、返回值和函数的定义体。被修饰的东西受强制保护,可以预防意外的变动,能提高程序的健壮性。

const只能修饰输入参数。若采用指针传递,可以起到保护指针的作用;若采用值传递(内部类型),将自动产生临时变量用于复制该函数,故不用const;对于外部类型,可以用const类修饰引用的方式,高效传递;

const修饰函数的返回值。若返回值是const修饰的指针,则指针内容不能改变,返回值只能赋给加const修饰的同类型指针;若返回值是值传递,不用const;

const修饰成员函数。任何不修改数据成员的函数都应该声明为const类型;

6、提升程序效率的规则
程序的时间效率是指运行速度,空间效率是指程序占用的内存或外存状况。全局效率指从操作系统的角度考虑的效率;局部效率指从模块或函数角度上考虑的效率。提高效率的规则如下:

应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高效率;

提高程序的全局效率为主,局部效率为辅;

先优化数据结构和算法,再处理程序代码;

不要追求紧凑的代码,因为这并不能产生高效的机器码;

7、C++中的四种类型转换
dynamic_cast(表达式):只用于对象、指针和引用,主要用于执行安全的向下转型。当用于多态类型时,它允许任意的隐式类型转换及相反的过程,但在反向转型时会检查操作是否有效,这种检测在运行时进行,若检测不是有效的对象指针,返回NULL;

static_cast(表达式):允许执行任意的隐式转换和相反的动作。应用在类指针上,父类指针和子类指针可以相互转换,转换过程中不检查。但是不能将const转化为非const;

const_cast(表达式):用来强制设置对象的常量性或移除;

reinterpret_cast(表达式):可转换一种类型的指针为其它类型的指针,也可以将指针转换为其它类型。非相关类型间的转换,不对操作内容进行检查。
 

这一篇主要对C++中使用最广泛的STL进行学习梳理,参照此文再配合STL参考手册,轻松掌握编程精要!

1、STL基本头文件
STL主要包含容器、算法和迭代器三个部分。容器实现了大多数数据结构;迭代器类似指针,通过它的有序移动将容器中的元素与算法关联起来,是实现STL的基础。常用的STL包含头文件如下:

STL包含文件均不含扩展名,其源文件位置一般是在编译器VC安装目录的include内。

2、模板
模板分为函数模板和类模板。函数模板与预处理的用法类似,提供编译过程中的文本替换功能,对类型有一定的保护;类模板可以编写通用的、类型安全的类。

STL的思想就是内存的动态分配、销毁、再分配,将内存管理部分进一步抽象,编成系统代码,应用过程中用户可不必明白内存的变化,不必自己编写代码来管理内存。

STL标准模板库强调软件的复用。Traits技术是采用的重要手段,它提取不同类的共性,以便能统一处理,它依赖将显式模板特殊化将不同的部分用统一接口来包装。STL中有大量的模板函数,故很多时候要重载与之对应的操作符。函数模板相当于应用框架,操作符重载相当于调用接口。

3、迭代器
迭代器是STL重要的核心技术,提供了统一访问容器元素的方法。迭代器即指针,可以是所需的任何类型,它是最大好处是使容器与算法分离。因为不同容器中完成相同功能代码的思路大体相同,将其抽象出来就产生了迭代器,这是泛型编程的思想。

在自己动手编写迭代器时,要在类中增加begin()、end()函数,用于获取起止迭代指针,要增加相应迭代器类,重载相关运算符,从而可以调用。

特定容器有特定的迭代器,故将迭代器作为内部类更适合应用。每个容器均有对应的迭代器,容器通过迭代器共享某一具体算法,而算法不具体依赖某一具体容器。由此可见,STL的编程思想是:1.形成容器元素;2.取出所需的迭代指针;3.调用通用算法。STL迭代器共分五大类型:

输入迭代器istream_iterator,按其顺序只能读取一次。构造函数可参阅STL库;

输出迭代器ostream_iterator,按顺序只写一次。构造函数可参阅STL库;

前向迭代器,这种迭代器包含了输入输出迭代器两者所有可能,可以对一个值多次读写。前向迭代器只能向前移动;

双向迭代器,具有前向迭代器的全部功能,它也可以向后移动操作;

随机访问迭代器,具有双向迭代器的全部功能,再加上一个指针的所有功能。

从以上可以看出,从前到后迭代器是一步步细化的,编程时根据实际情况选择适宜的迭代器,从而达到最高的效率。

4、输入输出流
标准输入流cin指键盘,标准输出流cout指显示器。运算符”<<”用作输入输出流的插入符,表明“输出到”;运算符”>>”用作提取符,表明“赋值给”。cin输入时,各数值间默认以空格为分界符的。

在交互过程中,一般要一次输入一行字符,为弥补cin的缺陷,可以改用get系统函数。get和getline函数的区别在于,get遇停止符时,不从输入流中提取界定符,而直接在末尾加”\0”标志,getline则从输入流中提取界定符,但不会将其放入结果缓冲区。

处理流错误:

C++对每次输入输出操作的结果都记录了其状态信息,主要函数如下:

文件输入输出流:

C++中文件读写用到的很多常数都在基类ios被定义出来,ifstream类只用于读取文件数据,ofstream类只用于写入数据到文件,fstream类则可以用于读取和写入文件数据。

文件打开的三个流都是用构造函数或open函数来实现,第一个参数是文件名,第二个参数是打开方式,打开方式如下:

文件关闭的三个流都是用close()函数释放文件资源;

文件的读写分为两种情况:当读写文本文件时,可直接创建输入输出文件对象来完成操作;当读写二进制文件时,主要通过流对象中的read、write函数实现,不过也要创建流对象;

定位输入输出流。流的位置标识有三个:

字符串输入输出流:

字符串输入输出流类是直接对内存而不是对文件或标准输出进行的操作。它使用cin、cout相同的格式函数来操作内存中的数据。全部的字符串流类声明都包含在标准头文件<sstream>中,标准库定义了三种串流:

Istringstream:字符串输入流,提供读string功能;

Ostringstream:字符串输出流,提供string写功能;

Stringstream:字符串输入输出流,提供读写string功能;

利用字符串输入输出流,可以方便地将多种基本数据类型组合成字符串,也可以反解字符串。

5、函数对象
将函数作为对象是程序设计的新的思维,STL通过重载类中operator函数实现函数对象的功能。标准C++库根据operator()参数个数为0,1,2划分,有如下几种函数对象:

a.    发生器:没有参数且返回任意类型值的函数对象,如随机函数发生器;

b.    一元函数:只有一个任意类型的参数,返回一个可能不同类型的函数对象;

c.    二元函数:两个任意类型参数,且返回一个任意类型的函数对象;

d.    一元判定函数:返回bool类型的一元函数;

e.    二元判定函数:返回bool类型的二元函数;

6、通用容器
STL提供了专家级的各种容器,功能更好,复用性更强,在应用程序开发过程中尽可能应用STL。

容器可以分为以下三类:

a.    序列性容器:按线性序列来存储某类型值的集合,每个元素都有自己特定的位置,顺序容器主要有vector、deque、list;

b.    关联式容器:它更注重快速高效的检索数据的能力,根据键值key来检索数据。容器中成员在初始化后都是按一定顺序排好的,关联式容器主要有set、multiset、map、multimap;

c.    容器适配器:对已有容器进行某些特性的再封装,它不是一个新容器,主要有stack、queue;

各容器的共性和区别:

各容器一般来说都有下列函数:默认构造函数、复制构造函数、析构函数、empty()、max_size()、size()、operator=、operator<、operator<=、operator>、operator>=、operator==、operator!=、swap()。

顺序容器和关联容器都共有下列函数:

a.    vector容器是动态数组,在堆中分配内存,元素连续存放,有保留内存。若减少大小后内存也不会释放,当元素个数大于当前容量时,会分配原来2倍容量,从而将元素复制到新空间,原空间释放。可以快速随机访问元素,快速在末尾插入删除元素,此过程中不用移动内存;对中间元素插入删除时要移动内存。若元素是结构体类型或类,移动内存时还要进行构造和析构操作。vector可以使用[]操作符。

b.    deque是双端队列,可以快速地随机访问元素,快速地在开始和末尾插入删除元素。随机插入比两端插入要慢,重新分配内存比vector要快。重新分配空间后,原空间保持,只是新建立了一个内存块,将新数据插入这个内存块,没有空间的复制删除过程,故deque是由多个分段连续的内存空间组成。因此,deque在某一位置上的操作是线性的,各小片内存间用链表相连,还是可以使用[],只是没有vector快。对deque排序,可以将其复制到vector中排完序后再复制回去。

c.    list是一种双线性列表,只能顺序访问,不支持随机访问。对每个元素分配空间,不存在空间不够和重新分配的情况。

d.    set是集合,支持快速查找,不允许有重复值,用平衡树结构来存储;multiset支持快速查找,允许有重复值。

e.    map是映射,一对一映射,支持关键字快速查找,不允许有重复值;multimap是一对多映射,基于关键字查找,允许有重复值。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值