C++重要知识点

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

但是事实往往并非如此,很多时候,一个程序的速度在框架设计完成时大致已经确定了,而并非是因为采用了C++语言才使其速度没有达到预期的目标。因此当一个程序的性能需要提高时,首先需要做的是用性能检测工具对其运行的时间分布进行一个准确的测量,找出关键路径和真正的瓶颈所在,然后针对瓶颈进行分析和优化,而不是一味盲目地将性能低劣归咎于所采用的语言。事实上,如果框架设计不做修改,即使用C语言或者汇编语言重新改写,也并不能保证提高总体性能。因此当遇到性能问题时,首先检查和反思程序的总体框架。然后用性能检测工具对其实际运行做准确地测量,再针对瓶颈进行分析和优化,这才是正确的思路。但不可否认的是,确实有一些操作或者C++的一些语言特性比其他因素更容易成为程序的瓶颈,一般公认的有如下因素。(1)缺页:如第四章中所述,缺页往往意味着需要访问外部存储。因为外部存储访问相对于访问内存或者代码执行,有数量级的差别。因此只要有可能,应该尽量想办法减少缺页。(2)从堆中动态申请和释放内存:如C语言中的malloc/free和C++语言中的new/delete操作非常耗时,因此要尽可能优先考虑从线程栈中获得内存。优先考虑栈而减少从动态堆中申请内存,不仅仅是因为在堆中开辟内存比在栈中要慢很多,而且还与“尽量减少缺页”这一宗旨有关。当执行程序时,当前栈帧空间所在的内存页肯定在物理内存中,因此程序代码对其中变量的存取不会引起缺页;相反,从堆中生成的对象,只有指向它的指针在栈上,对象本身却是在堆中。堆一般来说不可能都在物理内存中,而且因为堆分配内存的特性,即使两个相邻生成的对象,也很有可能在堆内存位置上相隔很远。因此当访问这两个对象时,虽然分别指向它们指针都在栈上,但是通过这两个指针引用它们时,很有可能会引起两次“缺页”。(3)复杂对象的创建和销毁:这往往是一个层次相当深的递归调用,因为一个对象的创建往往只需要一条语句,看似很简单。另外,编译器生成的临时对象因为在程序的源代码中看不到,更是不容易察觉,因此尤其值得警惕和关注。本章中专门有两节分别讲解对象的构造和析构,以及临时对象。(4)函数调用:因为函数调用有固定的额外开销,因此当函数体的代码量相对较少,且该函数被非常频繁地调用时,函数调用时的固定额外开销容易成为不必要的开销。C语言的宏和C++语言的内联函数都是为了在保持函数调用的模块化特征基础上消除函数调用的固定额外开销而引入的,因为宏在提供性能优势的同时也给开发和调试带来了不便。在C++中更多提倡的是使用内联函数,本章会有一节专门讲解内联函数。2.1构造函数与析构函数构造函数和析构函数的特点是当创建对象时,自动执行构造函数;当销毁对象时,析构函数自动被执行。这两个函数分别是一个对象最先和最后被执行的函数,构造函数在创建对象时调用,用来初始化该对象的初始状态和取得该对象被使用前需要的一些资源,比如文件/网络连接等;析构函数执行与构造函数相反的操作,主要是释放对象拥有的资源,而且在此对象的生命周期这两个函数都只被执行一次。创建一个对象一般有两种方式,一种是从线程运行栈中创建,也称为“局部对象”,一般语句为:{……Objectobj;①……}②销毁这种对象并不需要程序显式地调用析构函数,而是当程序运行出该对象所属的作用域时自动调用。比如上述程序中在①处创建的对象obj在②处会自动调用该对象的析构函数。在这种方式中,对象obj的内存在程序进入该作用域时,编译器生成的代码已经为其分配(一般都是通过移动栈指针),①句只需要调用对象的构造函数即可。②处编译器生成的代码会调用该作用域内所有局部的用户自定义类型对象的析构函数,对象obj属于其中之一,然后通过一个退栈语句一次性将空间返回给线程栈。另一种创建对象的方式为从全局堆中动态创建,一般语句为:{……Object*obj=newObject;①……deleteobj;②……}③当执行①句时,指针obj所指向对象的内存从全局堆中取得,并将地址值赋给obj。但指针obj本身却是一个局部对象,需要从线程栈中分配,它所指向的对象从全局堆中分配内存存放。从全局堆中创建的对象需要显式调用delete销毁,delete会调用该指针指向的对象的析构函数,并将该对象所占的全局堆内存空间返回给全局堆,如②句。执行②句后,指针obj所指向的对象确实已被销毁。但是指针obj却还存在于栈中,直到程序退出其所在的作用域。即执行到③处时,指针obj才会消失。需要注意的是,指针obj的值在②处至③处之间,仍然指向刚才被销毁的对象的位置,这时使用这个指针是危险的。在Win32平台中,访问刚才被销毁对象,可能出现3种情况。第1种情况是该处位置所在的“内存页”没有任何对象,堆管理器已经将其进一步返回给系统,此时通过指针obj访问该处内存会引起“访问违例”,即访问了不合法的内存,这种错误会导致进程崩溃;第2种情况是该处位置所在的“内存页”还有其他对象,且该处位置被回收后,尚未被分配出去,这时通过指针obj访问该处内存,取得的值是无意义的,虽然不会立刻引起进程崩溃,但是针对该指针的后续操作的行为是不可预测的;第3种情况是该处位置所在的“内存页”还有其他对象,且该处位置被回收后,已被其他对象申请,这时通过指针obj访问该处内存,取得的值其实是程序其他处生成的对象。虽然对指针obj的操作不会立刻引起进程崩溃,但是极有可能会引起该对象状态的改变。从而使得在创建该对象处看来,该对象的状态会莫名其妙地变化。第2种和第3种情况都是很难发现和排查的bug,需要小心地避免。创建一个对象分成两个步骤,即首先取得对象所需的内存(无论是从线程栈还是从全局堆中),然后在该块内存上执行构造函数。在构造函数构建该对象时,构造函数也分成两个步骤。即第1步执行初始化(通过初始化列表),第2步执行构造函数的函数体,如下:classDerived:publicBase{public:Derived():i(10),string(unnamed)①{...②}...private:inti;stringname;...};①步中的“:i(10),string(unnamed)”即所谓的“初始化列表”,以“:”开始,后面为初始化单元。每个单元都是“变量名(初始值)”这样的模式,各单元之间以逗号隔开。构造函数首先根据初始化列表执行初始化,然后执行构造函数的函数体,即②处语句。对初始化操作,有下面几点需要注意。(1)构造函数其实是一个递归操作,在每层递归内部的操作遵循严格的次序。递归模式为首先执行父类的构造函数(父类的构造函数操作也相应的包括执行初始化和执行构造函数体两个部分),父类构造函数返回后构造该类自己的成员变量。构造该类自己的成员变量时,一是严格按照成员变量在类中的声明顺序进行,而与其在初始化列表中出现的顺序完全无关;二是当有些成员变量或父类对象没有在初始化列表中出现时,它们仍然在初始化操作这一步骤中被初始化。内建类型成员变量被赋给一个初值。父类对象和类成员变量对象被调用其默认构造函数初始化,然后父类的构造函数和子成员变量对象在构造函数执行过程中也遵循上述递归操作。一直到此类的继承体系中所有父类和父类所含的成员变量都被构造完成后,此类的初始化操作才告结束。(2)父类对象和一些成员变量没有出现在初始化列表中时,这些对象仍然被执行构造函数,这时执行的是“默认构造函数”。因此这些对象所属的类必须提供可以调用的默认构造函数,为此要求这些类要么自己“显式”地提供默认构造函数,要么不能阻止编译器“隐式”地为其生成一个默认构造函数,定义除默认构造函数之外的其他类型的构造函数就会阻止编译器生成默认构造函数。如果编译器在编译时,发现没有可供调用的默认构造函数,并且编译器也无法生成,则编译无法通过。(3)对两类成员变量,需要强调指出即“常量”(const)型和“引用”(reference)型。因为已经指出,所有成员变量在执行函数体之前已经被构造,即已经拥有初始值。根据这个特点,很容易推断出“常量”型和“引用”型变量必须在初始化列表中正确初始化,而不能将其初始化放在构造函数体内。因为这两类变量一旦被赋值,其整个生命周期都不能修改其初始值。所以必须在第一次即“初始化”操作中被正确赋值。(4)可以看到,即使初始化列表可能没有完全列出其子成员或父类对象成员,或者顺序与其在类中声明的顺序不符,这些成员仍然保证会被“全部”且“严格地按照顺序”被构建。这意味着在程序进入构造函数体之前,类的父类对象和所有子成员变量对象都已经被生成和构造。如果在构造函数体内为其执行赋初值操作,显然属于浪费。如果在构造函数时已经知道如何为类的子成员变量初始化,那么应该将这些初始化信息通过构造函数的初始化列表赋予子成员变量,而不是在构造函数体中进行这些初始化。因为进入构造函数体时,这些子成员变量已经初始化一次。下面这个例子演示了构造函数的这些重要特性:#includeiostreamusingnamespacestd;classA{public:A(){coutA::A()endl;}};classB:publicA{public:B():j(0){coutB::B()endl;}private:intj;};classC1{public:C1(inti):a(i){coutC1::C1()endl;}private:inta;};classC2{public:C2(doubleval):d(val){coutC2::C2()endl;}private:doubled;};classC3{public:C3(intv=0):j(v){coutC3::C3()endl;}private:intj;};classD:publicB{public:D(doublev2,intv1):c2(v2),c1(v1){coutD::D()endl;}②private:C1c1;C2c2;C3c3;};intmain(){Dd(1.0,3);①return0;}在这段代码中,类D继承自类B,类B继承自类A。然后类D中含有3个成员变量对象c1、c2和c3,分别为类型C1、C2和C3。此段程序的输出为:A::A()③B::B()④C1::C1()⑤C2::C2()⑥C3::C3()⑦D::D()⑧可以看到,①处调用D::D(double,int)构造函数构造对象d,此构造函数从②处开始引起了一连串的递归构造。从输出可以验证递归操作的如下规律。(1)递归从父类对象开始,D的构造函数首先通过“初始化”操作构造其直接父类B的构造函数。然后B的构造函数先执行“初始化”部分,该“初始化”操作构造B的直接父类A,类A没有自己的成员需要初始化,所以其“初始化”不执行任何操作。初始化后,开始执行类A的构造函数,即③的输出。(2)构造类A的对象后,B的“初始化”操作执行初始化类表中的j(0)对j进行初始化。然后进入B的构造函数的函数体,即④处输出的来源。至此类B的对象构造完毕,注意这里看到初始化列表中并没有“显式”地列出其父类的构造函数。但是子类在构造时总是在其构造函数的“初始化”操作的最开始构造其父类对象,而忽略其父类构造函数是否显式地列在初始化列表中。(3)构造类B的对象后,类D的“初始化”操作接着初始化其成员变量对象,这里是c1,c2和c3。因为它们在类D中的声明顺序就是c1-c2-c3,所以看到它们也是按照这个顺序构造的,如⑤,⑥,⑦3处输出所示。注意这里故意在初始化列表中将c2的顺序放在了c1的前面,c3甚至都没有列在初始化列表中。但是输出显示了成员变量的初始化严格按照它们在类中的声明顺序进行,而忽略其是否显式地列在初始化列表中,或者显示在初始化列表中的顺序如何。应该尽量将成员变量初始化列表中出现的顺序与其在类中声明的顺序保持一致,因为如果使用一个变量的值来初始化另外一个变量时,程序的行为可能不是开发人员预想的那样,比如:classObject{public:Object():v2(5),v1(v2*3){…}priv

1 / 53
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功