笔记60-70

61. 关于函数内联和宏

   C程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。对于C++ 而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。

  C++中,在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。

  C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在C++ 程序中,应该用内联函数取代所有宏代码,“断言assert”恐怕是唯一的例外。assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。如果assert是函数,由于函数调用会引起内存、代码的变动,那么将导致Debug版本与Release版本存在差异。所以assert不是函数,而是宏。

  inline是一种“用于定义的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。仅仅在声明函数时在前面加上inline是没有用处的。要在函数的定义时在前面加上inline才有用。因此不建议在函数的声明处加inline,因为用户可以查看函数的声明不能看函数的定义,用户没有必要也不能知道该函数是不是内联函数。

  以下情况不宜使用内联:

1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

62.拷贝构造函数

C++中,下面三种对象需要调用拷贝构造函数:
  1) 一个对象以值传递的方式传入函数体;
  2) 一个对象以值传递的方式从函数返回;
  3) 一个对象需要通过另外一个对象进行初始化;

有时候默认的拷贝构造函数会有问题,比如类的数据成员有指针,指向一块动态申请的内存时。

class CExample
  
{
  
public:
  
    CExample(){pBuffer=NULL; nSize=0;}
  
    ~CExample(){delete pBuffer;}
  
    void Init(int n) { pBuffer=new char[n]; nSize=n;}
  
private:
      char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源

      int nSize;
  };

int main(int argc, char* argv[])
  
{
  
    CExample theObjone;
  
    theObjone.Init(40);
      //现在需要另一个对象,需要将他初始化称对象一的状态

      CExample theObjtwo=theObjone;//调用拷贝构造函数 
  }

最后一句中,首先调用CExample的构造函数,构造theObjttwo.然后调用默认的拷贝构造函数,执行theObjtwo.pBuffer==theObjone.pBuffer。即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患(不能两次delete/free同一块内存)。这种情况下,需要自己写拷贝构造函数了。

  class CExample
  
{
  
public:
  
     CExample(){pBuffer=NULL; nSize=0;}
  
     ~CExample(){delete pBuffer;}
       CExample(const CExample&); //拷贝构造函数

       void Init(int n){ pBuffer=new char[n]; nSize=n;}
  
private:
       char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源

       int nSize;
  
};
  CExample::CExample(const CExample& RightSides) //拷贝构造函数的定义

  {
       nSize=RightSides.nSize; //复制常规成员

       pBuffer=new char[nSize]; //复制指针指向的内容
       memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
  }

63.类的赋值函数

           String a,b;

    String  c = a; // 调用了拷贝构造函数,最好写成 c(a);

            c = b; // 调用了赋值函数

            本例中第二个语句的风格较差,宜改写成String c(a) 以区别于第三个语句。

// 赋值函数

    String & String::operate =(const String &other)

    {  

        // (1) 检查自赋值

        if(this == &other)

            return *this;

       

        // (2) 释放原有的内存资源

        delete [] m_data;

       

        // 3)分配新的内存资源,并复制内容

    int length = strlen(other.m_data); 

    m_data = new char[length+1];

        strcpy(m_data, other.m_data);

       

        // 4)返回本对象的引用

        return *this;

}  

   

    String拷贝构造函数与普通构造函数的区别是:在函数入口处无需NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL

    String的赋值函数比构造函数复杂得多,分四步实现:

(1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现。

2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。

3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘/0’。函数strcpy则连‘/0’一起复制。

4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?

不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。

 
64.构造和析构的次序

   构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。

   一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。

65.在派生类中实现基类的4个基本函数

   基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:

u       派生类的构造函数应在其初始化表里调用基类的构造函数。

u       基类与派生类的析构函数应该为虚(即加virtual关键字)。例如:

#include <iostream.h>

class Base

{

  public:

    virtual ~Base() { cout<< "~Base" << endl ; }

};

class Derived : public Base

{

  public:

    virtual ~Derived() { cout<< "~Derived" << endl ; }

};

void main(void)

{

    Base * pB = new Derived;  // upcast

    delete pB;

}

 

输出结果为:

       ~Derived

       ~Base

如果析构函数不为虚,那么输出结果为

       ~Base

 

u       在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:

Derived & Derived::operate =(const Derived &other)

{

    //1)检查自赋值

    if(this == &other)

        return *this;

 

    //2)对基类的数据成员重新赋值

    Base::operate =(other); // 因为不能直接操作私有数据成员

 

    //3)对派生类的数据成员赋值

    m_x = other.m_x;

    m_y = other.m_y;

    m_z = other.m_z;

 

    //4)返回本对象的引用

    return *this;

}

66.类的继承和组合

   继承规则:若在逻辑上BA的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。

   组合:若在逻辑上AB的“一部分”(a part of),则不允许BA派生,而是要用A和其它东西组合出B

  

组合关系

继承关系

局部类

父类

整体类

子类

从整体类到局部类的分解过程

从子类到父类的抽象过程

从局部类到整体类的组合过程

从父类到子类的扩展过程

 

组合关系

继承关系

优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立

 

缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性

 

优点:具有较好的可扩展性

 

缺点:支持扩展,但是往往以增加系统结构的复杂度为代价

 

优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象

优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口

 

缺点:不支持动态继承。在运行时,子类无法选择不同的父类

缺点:子类不能改变父类的接口

 

缺点:整体类不能自动获得和局部类同样的接口

 

优点:子类能自动继承父类的接口

 

缺点:创建整体类的对象时,需要创建所有局部类的对象

 

优点:创建子类的对象时,无须创建父类的对象

 

67. 关于常量折叠

#include <iostream>
using namespace std;
int main(int argc,char*argv[])
{
  const int a=1;
  int*p=(int*)&a;
  cout < <"source:a=" < <a < <" " < <"&a=" < <&a < <endl;
  cout < <"source:*p=" < <*p < <" " < <"p=" < <p < <endl;
  (*p)++;
  cout < <"modify:a=" < <a < <" " < <"&a=" < <&a < <endl;
  cout < <"modify:*p=" < <*p < <" " < <"p=" < <p < <endl;
  return 0;
}
执行结果是:
source:a=1 &a=0xbfb50568
source:*p=1 p=0xbfb50568
modify:a=1 &a=0xbfb50568
modify:*p=2 p=0xbfb50568

为什么a*p的地址是相同的会输出不同的内容呢?问题在于编译器!C++有一个定义 1是常量表达式(文字常量都是常量表达式),如果一个 常量 被常量表达式初始化,那这个常量 也就是一个常量表达式.编译器在处理常量表达式的时候,都会直接替换成文字常量,而不会从内存中读取数值.这从编译器的角度来说,叫做常量折叠,为了优化速度

68. 值拷贝与位拷贝(不同数据类型)
    float ft = 1.0f;

    int  it;

    it = (int)ft;     // 按值拷  

it = *(int*)&ft; // 按位拷

位拷贝是将A内存原封不动的搬到B区域,但是B区域的数据类型和A是不同的,对相同的内存内容的理解也就不同。比如:00 00 80 3F,对于float型变量来说,理解为1.000000,对于int型变量来说理解为1065353216.

值拷贝的方法:强制数据类型转换

位拷贝的方法:强制地址类型转换,然后取值。

69. 隐式加载dll文件   

    首先新建一个Win32 Dynamic_Link Library工程Dll1,添加dll1.cpp编写一个函数

_declspec(dllexport) int add(int a, int b)

{return a+b;}

    编译,会产生两个文件Dll1.LibDll1.Dll。可以再命令提示行中利用dumpbin -exports Dll1.Dll产看该dll有哪些导出函数。

    新建一个MFC工程,添加一个按钮btn1,添加消息响应函数Onbtn1,在消息响应函数的前面添加一句extern int add(int a, int b);把要用到的函数声明为外部函数。在Onbtn1中就可以调用该函数了。

为了使函数能够执行,需要把Dll1.LibDll1.Dll两个文件拷贝到MFC的工程目录下。并且在project->settings->link->Object/library modules中加入dll1.Lib。这样就可以正确的调用dll文件中的函数了。

如果dll有头文件,可以采用:拷贝.dll.lib;加载.lib;包含头文件;三个步骤就可以达到隐式调用dll的目的。

70. 显示加载dll文件
  
不用包含dll头文件,不用加载.lib文件,不用extern声明外部函数。仅需要在需要的地方加载.dll文件就可以了。

   HINSTANCE hInst;//定义.dll文件的句柄

   hInst = LoadLibrary("Filename.dll");//需要把.dll文件提前拷贝到工程目录下

   Typedef int (*ADDPROC) (int a, int b);//定义函数指针

   ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add");// adddll中的导  

   //出函数名字,要注意函数名字的改编,Add为在该程序中使用的新名字。

   //如果dll中使用的调用约定和本程序编译器使用的调用约定不同时,要在定义函    //数指针时显示的加上调用约定,vc6.0默认_cdecl约定。

   ...................................//先判断Add是否为NULL,然后可以调用Add了。

隐式加载和显示加载的区别:

   显示加载只需要.dll文件,不需要.lib.h文件。在程序访问多个dll问价时,如果采用隐式加载,程序启动会非常的慢,因为需要加载所有的dll库。而显示加载只是在需要的地方才加载所要的dll文件。当然显示加载一次仅仅能够导出一个函数,如果需要用到某个dll中的大量函数,还是采用隐式加载比较方便。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值