《c++应用程序性能优化》读书笔记(一)

#一个程序占用的内存区一般分为以下5部分:
1.全局/静态数据区。
        存储全局变量和静态变量(包括全局静态变量和局部静态变量),编译期间已分配好空间。
2.常量数据区。
        存储程序中的常量字符串等,编译期间已分配好空间。常量区的数据不可改变。
3.代码区。
4.栈。
5.堆。
 
具体哪些变量存放在何处,可通过打印以下程序中各个变量的地址进行分析:
...
int nGlobal=6;
int main()
{
char **p1="str1";
const char *p2="str2";
static int nStaticLocal1= 7;
int nLocal2=8;
const int nLocal2=9;/*注意,虽然nLocal2前有修饰符const,但它并不存放在常量数据区中,而是放在栈上,const并没有改变该变量是变量的本质,只是限制其不可更改。nLocal2与常量字符串"str1"还是有区别的*/
int *p3=new int[5];/*书中说32位系统,堆按16字节对齐。因此,虽然p3指向的数组大小为20个字节,但要占32字节空间*/
char *p4=(char *)malloc(3);
}
 
分别打印&nGlobal, &p1,&p2,&nStaticLocal1,&nLocal2,&p3,&p4,p1,p2,p3,p4。
 
#静态变量在第一次进入其作用域时会被初始化,已后再次进入其作用域时不必初始化,其内容为最近更改过的值。
 
#一个类的多个对象间共享数据,可使用全局变量,但这会破坏类的封装性。因此,C++提供了类的静态成员变量,以便在类的多个对象间共享数据。类的静态成员变量存储在全局/静态存储区,只有一份拷贝,由类的所有对象共享。
 
#栈和堆的几点区别:
 
1.大小:栈的大小多半固定,具体大小由编译器决定。visual studio 2003下栈的大小默认为1MB,可通过编译选项改变,但通常不会太大。下面的程序可能会导致栈溢出:
int main()
{
    ...
    char buf[1024*1024];//声明一个栈上1MB的数组,可能会导致栈溢出
    ...
}
 
堆在大小一般只受限于系统有效的虚拟内存的大小,可用来创建一些内存较大的对象或数据。
 
2.效率:栈上内存由系统自动分配,效率高,内存空间连续,不产生内存碎片。堆上内存由开发人员分配和释放。堆上分配内存时,系统按一定的算法在堆空间中寻找合适大小的空闲堆,并修改维护堆空闲空间的链表。因此,效率低些,且易产生内存碎片。此外,应尽可能从栈上申请内存,因为栈帧空间肯定在物理内存中,而堆却不一定。在堆上申请对象时,由于堆不可能全在内存中,可能会产生缺页,两个在堆上申请的对象也可能地址上不连续,相隔很远。
 
#全局变量对象在调用main()函数前被创建,在退出main()时销毁。静态对象在进入其作用域时创建,在main()函数退出后销毁。一般对象在进入其作用域时创建,退出其作用域时销毁。
 
#有以下程序:
 
class A
{
    public:
        A()    {cout<<"A created"<<endl;};
        A(A& a) {cout<<"A created with copy"<<endl;};
        ~A()    {cout<<"A destroyed"<<endl;}; 
};
 
A foo(A a)
{
    A b;
    return b;
 
int main(void)
{
    A a;
    a=foo(a);
    return 0;
}
 
共运行结果为:
A created
A created with copy
A created
A created with copy
A destroyed
A destroyed
A destroyed
A destroyed
 
共有4个对象被创建。从结果易知,多出的两个对象是拷贝构造函数创建的。由于foo函数的参数是通过值传递的。当调用foo函数时,实参要复制一份,并压入foo函数栈中,这得调用一次拷贝构造函数。当foo函数返回时,由于foo中b对象在函数退出时将销毁,因此为了能够将返回值传递给main函数中的a对象,得在栈中建立一个临时对象作为b的副本,这又得调用一次拷贝构造函数。
 
以上程序只创建了两个临时对象,问题貌似不大。但如果上述代码出现在某个循环中时,那么将可能有大量的临时对象被创建和销毁,这必将影响程序的性能。解决上述问题的方法是将foo函数的参数设置为引用类型,即foo(A& a)。在实际开发中,应尽可能减少临时对象的创建。
 
#有以下对象,
 
class simpleClass
{
    public:
        static int nCount;
        int nValue;
        char c;
        
        simpleClass();
        virtual ~simpleClass();
        int getValue(void);
        virtual void foo(void);
        static void addCount();
};
 
该类在内存中的布局如下图所示:
 
 
若有以下类继承了simpleClass:
 
class derivedClass:public simpleClass
{
    public:
        int nSubValue;
        derivedClass(){...};
        ~derivedClass(){...};
        virtual void foo(void);
};
 
则类derivedClass的对象中变化的部分的内存布局为(没变的部分derivedClass全盘从父类中接收):
 
 
 
#设有如下继承关系,derivedClass继承了simpleClass1和simpleClass2。nSubValue为derivedClass成员,foo1为simpleClass1的虚函数, foo2为simpleClass2的虚函数。derivedClass重写了foo2,derivedClass对象的内存关系图为:
 
#有如下继承关系,基类baseClass,midClass1继承了baseClass,midClass2继承了baseClass,derivedClass继承了midClass1和midClass2。即所谓的“菱形继承”。那么,derivedClass对象的内存分布为:
 
 
这种情况下很容易出错。设baseClass有函数foo(), derivedClass对象a,如要调用baseClass中的foo函数,写成a.foo()会报错,应写成a.midClass1::foo()或a.midClass2::foo()的行式。而且,通过midClass1修改了在baseClass中定义的某个成员变量,midClass2中的baseClass成员变量并不会跟着改变。
 
为了改进“菱形继承”的缺点,C++提供了虚拟继承的概念。即原midClass1和midClass2对baseClass的继承应写成:
 
class midClass1:virtual public baseClass
{
    ...
};
 
class midClass2:virtual public baseClass
{
    ...
};
 
derivedClass的继承写法不变:
class derivedClass: public midClass1,public midClass2
{
    ...
};
 
那么,derivedClass对象的内存布局为:


baseClass放在了derivedClass对象的地址的最下面。注意各部分的存放位置,它们之间是有顺序的。
 
#创建一个对象分为两个步骤,首先取得对象所需的内存空间,然后在该内存空间上执行构造函数。构造函数构建对象时也分成两步:
 
1.执行初始化成员变量操作(通过初始化列表),成员变量初始化的顺序与它们在类中定义的顺序一致,而不是按初始化列表中的顺序。尽管初始化列表可能没有完全列出所有的成员变量或父类对象,或者是顺序与它们在类中的声明顺序不一致,但这些成员仍然会保证被全部构建。
 
2.执行构造函数的函数体。注意,对象的所有的成员变量在执行构造函数的函数体之前已经被赋予初始值(即在初始化列表中)。根据这个特点可知,在类中定义的引用型和常量型成员变量一定在要第1次即初始化列表操作中被赋值。
 
由于在第1步中所有成员已在初始化列表中被生成和构造,因此如果在第2步的函数体中再对这些成员变量赋值显然属于浪费。尽量在初始化列表中对所有成员进行初始化,毕竟在进行函数体前,所有成员已被初始化了一次。
 
#减少临时对象的创建对提高程序的效率作用很大。因类型不匹配而生成临时对象的情况,可从以下程序来认识。设有如下类:
 
class Rational
{
    public:
        Rational (int a=0, int b=1): m(a), n(b){}        //1
    private:
        int m;
        int n;
};
...
void foo(void)
{
    Rational r;
    r=100;            //2
    ...
}
 
当程序执行到2处的代码时,由于该类没有重载operator=(int i),所以编译器会生成默认的operator=(Rational& a)函数。但编译器生成的赋值函数的参数类型为Rational&,不是int型,因此2处代码原则上说无法通过编译。但是,C++编译器在判定2处的语句能否通过编译前,总是尽可能地查找可能的转换路径,以满足编译需要。这样,编译器发现1处的构造函数可接受0、1、或2个整数作为参数,因此会将100传递给1处的构造函数,通过调用Rational(100,1)生成一个临时对象,然后执行2处的语句。
 
从以上代码可以看出,为了正确进行编译,编译器悄悄进行了很多处理。这在一定程度上加快了程序的编写,提高了开发速度,但临时对象创建过多将导致程序性能下降。要想阻止这种自动类型转换的发生,可在1处的前面加上关键字explicit,即:
explicit Rational (int a=0, int b=1): m(a), n(b){}
explicit的含义是开发人员只能显示地根据函数的定义进行调用,不允许编译器进行隐式的类型转换。因此,要想成功编译2处语句,要么写成r=Rational(100),要么重载operator=(int i)。重载operator=(int i)可写成以下形式:
Rational& operator=(int i)
{
    m=i;
    n=1;
    return *this;
}
这样,重载函数不会产生临时对象。
 
#有如下类:
 
class Rational
{
    friend const Rational operator+(const Rational& a,const Rational& b){}
    public:
        Rational (int a=0, int b=1): m(a), n(b){}
        Rational (const Rational& r): m(r.m), n(r.n){}
        Rational& operator=(Rational& a){} 
    private:
        int m;
        int n;
};
 
const Rational operator+(const Rational& a,const Rational& b)        //2
{
    Rational temp;
    temp.m=a.m+b.m;
    temp.n=a.n+b.n;
    return temp;    //6
}
 
int main(void)
{
    Rational r,a(10,10),b(2,3);//3
    r=a+b;        //1
    return 0;
}
 
从以上程序易知,语句3的执行调用了3次构造函数。执行语句1时,进入函数2后创建temp会调用一次构造函数,该函数返回时又调用了一次拷贝构造函数。该程序调用的构造函数次数太多,系统开销大,必须对其进行改进。
 
从上述代码不难看出,对象r在3处定义,但直到1处才被引用。其实,r没必要定义太早,因为其在被构造后没有马上被用到,因此完全可将其延迟到被引用时再生成。改进后的main函数为:
 
int main(void)
{
    Rational a(10,10),b(2,3);//5
    Rational r=a+b;        //4
    return 0;
}
 
main函数改写后的好处是,编译器不再把语句4中的'='当作赋值运算符,而是把语句4当作对象r的初始化语句(c++中,a=b相当于赋值语句,调用的是赋值运算符,可根据需要重载;type a=b相当于初始化语句,调用的是拷贝构造函数,也可根据需要重载)。这样,在执行完函数2后,该程序并不会在main函数的栈帧中开辟空间以存储拷贝自temp的临时对象(减少了一次拷贝构造函数的调用),而是直接使用r对象的空间在6处用temp作为拷贝构造函数的参数对r进行初始化。这样,函数2返回时没必要对temp进行拷贝来生成临时对象,也不用像原来的语句1那样调用赋值函数。
 
启示:对于非内建类型的对象,尽量延迟其定义,这可减少临时对象的生成。
 
以上程序还可再进一步优化,将重载操作符函数operator+()写成:
 
const Rational operator+(const Rational& a,const Rational& b)    //7
{
    return Rational(a.m+b.m,a.n+b.n);    //8
}
 
由于编译器在执行语句4时,认为该语句是初始化操作,而不是赋值语句。所以,编译器在传递参数给函数7时,也传入了在main函数中为r对象开辟的内存空间地址。这样,语句8的执行实际是在对象r的空间上进行的,即构造了r对象。这样,可省去计算temp对象。
 
需要注意的是,这种优化要和语句4处的优化配合起来才有效,即a+b是用来初始化对象的,而不是用来进行赋值的。如果4处依然是1处的写法,那么虽然7处的函数中没有定义temp,但语句8执行后还是会存储一个临时变量到栈帧中,在函数返回后将该临时对象拷贝给r。
 
#有如下程序:
string a,b;
const char* str;
...
if(strlen(str=(a+b).c_str())>5)    //1
{
    printf("%s",str);
    ...
}
 
上述代码中,语句1不正确。因为a+b是临时字符串,在语句1执行完后就不存在了,str指向的内存已被收回。但这条规则有个特例,即当用一个临时对象来初始化一个常量引用时,该临时对象的生命期将和常量引用的生命期等长。如:
string a,b;
..
if(..)
{
    const string& c=a+b;
    cout<<c<<endl;
    ...
    
}
 
a+b作为一个临时对象,在if语句执行完后不会销毁,而是保持和c相同的生命期。
 
 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值