讨论记录之C++细节

来源:互联网 发布:期货自动化交易软件 编辑:程序博客网 时间:2024/05/17 07:09

Participants:LF,HZP,CPP,ZY

Date:08-09-16  7:20PM

Recorder: CPP,ZY

参考文献:

1、《effective C++》2nd Edition,Scott Meyers etc.

2、《C++程序设计教程》,钱能

3、《高质量C++C编程指南》,林锐

4、http://keith.ecjtu.com/article.asp?id=319

 应大家的要求,今天晚上开始了我们的第一次讨论会。

主要是针对C++里面的一小撮问题展开的,这里我给出讨论的概要:

1、关于优先级与结合性;(这里要重点“批”一下HZP和ZY,正号和加号都分不清的家伙…)(顶)

2、#define /inline/const(顺便涉及到inline 和 virtual的连用问题);

3、const的作用(包括修饰类的成员函数,成员函数返回值,成员函数的参数列表,数据成员);

4、重载、覆盖(重写/改写,实现多态)以及隐藏的区别;

5、构造函数和析构函数

       6、关于虚拟函数、虚基类及多继承

1、优先级口诀:(除了标明是右结合外,都是左结合)

括号成员第一;//[]、()

全体单目第二;//比如++、--、+(正号)、-(负号)、指针运算符* & 右结合

乘除余第三;//取余 左结合

移位五,关系六;

等于不等排第七;

位与亦或和位或;//&、 ^、|

逻辑或跟与;//&&、||

条件高于赋值;//注意的是赋值运算符很多,包括= 、*=、 /=、 +=、 -= 、|=、 <<=和>>=  二者都是右结合

逗号排最后。

上面是C中的规则,而C++由于引入了一些新的运算符,因此,有些出入,如表1:

 

 

表1 C++ 运算符优先级列表

见两个例子:

(1) int x=1,y=0;

!x&&x+y&&++y;

加括号确定优先级的方法
  当多个优先级不同的运算符在一起时,为了不混淆,可以先加上括号,这样就分出层次了,相同层次的考虑结合性问题,当确定下来先算那块时,再往这块里面深入。例如上面的例子,我们可以这样加上括号:从左向右看,由于!比&&优先级高,所以有(!x),又由于&&比+优先级低,所以有(x+y),而++优先级高于&&,所以(++y)。这样整个式子就变成了:(!x)&&(x+y)&&(++y),最外层的是两个&&运算,由于&&的结合性是从左至右,所以上式可看成:A&&B&&C,先计算A,再计算B,最后算C.由于x=1,则!x就为假,后面的就不需要再算了,整个语句的值为假。执行完后,y的值没变,还是0.
  所以碰到不清楚先算谁后算谁时,先加个括号看看,就明白了先后次序。

(2)给语句c=a>b?a:b;加括号。此语句有三个运算符:=、>、? :,应该怎样加括号呢?

 第一种方案:c=((a>b)?a:b);
  第二种方案:c=(a>(b?a:b));
  第三种方案:(c=a)>(b?a:b);
  应该是那一种呢?按照运算符优先级的高低顺序,>优先级高于=,所以不可能把(c=a)括起来。而>优先级高于? :运算符。所以也不可能把(b?a:b)括起来。因此,第一种答案正确。

 

2、尽量以const和inline取代#define

尽量以编译器取代预处理器或许更好,因为#define通常不被视为语言本身的一部分。

#define导致的结果就是程序内所使用的名称并未出现于符号表之中。可以改用常量来声明。

若是需要一个class专属常量,即将这个常量的scope局限于class 之内,必须让它成为一个member,而为了确保这个常量至多只有一份实体,则必须让他成为一个static member,例如:
class GamePlayer{

private:
       static const int NUM;//仅仅是个声明而非定义

    …

};

必须在类定义文件中定义该类成员:const int GamePlayer::NUM=5;

     另一个误用#define指令的常见例子是,以它来实现宏——看起来像函数,却又不会带来函数调用所需的成本。经典例子就是计算两数的最大值:

#define  max(a,b)  ((a)>(b)?(a) : (b))

即使加上了小括号,还是会发生一个可怕的动作:

int a=5,b=0;

max(++a,b);//a被累加两次

max(++a,b+10);//a被累加一次

这时,我们可以使用inline函数,既可以得到宏带来的高效率以及函数带来的可预期行为和类型检验。例如:

inline int max(int a, int b) {return a>b?a:b; }

这与前述宏并不完全相同,因为这个版本的max只接受int类型参数,不过,template可以修正这一问题,这里by reference相比by value可以获取更高的效率。

template<class T>

inline const T& max(const T& a, const T&b)

{return a>b?a:b; }

 

3、const的作用

(1)修饰类的成员函数时:

即在成员函数声明时将const置于成员函数参数列表后分号前,代表它不能对类的数据成员进行修改,但是有一个例外,就是当数据成员前有mutable修饰时,它是可以被该函数修改的;

(2)修饰成员函数返回值及函数参数时:

意即被修饰的量是不可被修改的,前者意味着在成员函数返回后得到的值不可被更改,后者意味着不能在函数体内对参数进行变动,只能读取它;

(3)对于类中的const常量,它只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。

     不能在类声明中初始化const数据成员,只能在类构造函数的初始化表中进行,例如:

class A

{…

const int SIZE=100;//错误,企图在类声明中初始化const数据成员

int array[SIZE];//错误,位置的SIZE

};

应该是:

class A

{

A(int size);

const int SIZE;

};

A::A(int size):SIZE(size){

}

若要建立在整个类中都恒定的常量,需要用枚举常量来实现,例如:

class A{

       enum{SIZE1=100,SIZE2=200};//

       int array1[SIZE1];

       int array2[SIZE2];

};

枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数.(PI=3.14159)

 

4、重载(overload)、覆盖(override)以及隐藏

本来是讨论多态的,但是我们又讲到了重载这个概念,对于一个类中的成员函数,其被重载的特征:

(1)相同的范围(同一个类中);

(2)函数名相同,参数列表不同,返回值类型可相同也可不同。

覆盖是指派生类函数覆盖基类函数,其特征:

(1)不同范围;

(2)函数名字相同,参数列表相同,

(3)基类必须要有virtual关键字。

除覆盖外,所有同名的基类函数与子类函数都属于隐藏,下面是一个例子,讲得比较清楚,也点出了问题的本质:

class Base  
{
public:
 Base();
 virtual ~Base();
 public:
 virtual void f(float x)
 {
  cout << "Base f(float)" << x <<endl;
 }
 void g(float x)
 {
  cout<< "Base g(float)" << x <<endl;

 }
 void h(float x)
 {
  cout<< "Base h(float)" << x <<endl;
 }

};


派生类:

class Derived:public Base
{
public:
 Derived();
 virtual ~Derived();
public:
 virtual void f(float x)
 {
  cout << "Derived f(float)" << x <<endl;
 }
 void g(int x)
 {
  cout<< "Derived g(int)" << x <<endl;

 }
 void h(float x)
 {
  cout<< "Derived h(float)" << x <<endl;
 }
};

 Derived  d;
 Base *pb = &d;
 Derived *pd = &d;

 pb->f(3.14f); // Derived f(float)3.14 调用派生类函数
 pd->f(3.14f); // Derived f(float)3.14 调用派生类函数

 pb->g(3.14f); // Base g(float)3.14 (!) 调用基类函数
 pd->g(3.14f); // Derived g(int)3       调用派生类函数  

 pb->h(3.14f); // Base h(float)3.14 (!) 调用基类函数
 pd->h(3.14f); // Derived h(float)3.14  调用派生类函数

 


总结:

当基类函数和子类函数间的关系为“覆盖”时,是根据对象类型来调用相应的函数;

而当基类函数和子类函数间的关系为“隐藏”时,是根据指针类型来调用相应的函数。

 

5、构造函数和析构函数

几乎每个类都会有一个或多个构造函数,一个析构函数。前者用来建立和初始化对象,只要对象建立,它马上被调用,给对象分配空间和初始化。

如果一个类没有专门定义构造函数,那么C++就仅仅创建对象而不作任何初始化。

(1)            假设一个类的构造函数一个都未提供,则C++提供一个默认的构造函数,该默认构造函数是个无参构造函数,它仅负责创建对象,而不做任何初始化工作;

(2)            只要一个类定义了一个构造函数(不一定是无参构造函数),C++就不再提供默认的构造函数,也就是说,如果为类定义了一个带参数的构造函数,还想要无参构造函数,则必须自己定义。

(3)            与变量定义类似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的位模式全为0,否则,对象是随机的。

(4)            如果类内动态分配内存,需为此类声明一个copy constructor和assignment运算符,请看图1:

Class String{

public:

String(const char *value);

~String();

private:

char *data;

};

      Assignment运算符:

这个class之中并无assignment运算符或是copy constructor,这样子会带来严

重后果,假设我们定义了两个对象,形式如下:

String  a(“Hello”);

String  b(“World”);

对象a内有一个指针,指向一块内含字符串“Hello”的内存。对象b也是内含了一个指针,指向一块内含字符串“World”的内存。现在执行assignment动作:b=a;

由于自定义的operator=并不存在,所以C++产生一个默认的assignment运算符并调用它。这个默认的assignment运算符对着a的menmbers执行一个member一个member的逐次赋值动作,将内容赋给b的members。对指针(a.data和b.data)而言,其实就只是一个bit一个bit的逐次拷贝动作。Assignment后的结果就如图2所示:

 

 

 

 

     a
 
data
 
H e l l o /0
 
 

 

 

     b
 
data
 
World /0
 

                            图1

 

 

 

 

     a
 
data
 
H e l l o /0
 
 

 

 

     b
 
data
 
World /0
 

                              图2

这样的状况至少存在两个问题,第一,b原先所指的内存并没有被释放掉;它永远遗失了,这是一个典型的memory leak问题,第二,a和b内含的指针如今指向相同的字符串,因此,当其中一个离开生存空间时,其destructor会删除内存,而此内存目前仍被另一个指针所指,例如:

String a(“Hello”);//定义并构造a

{

       String b(“World”);//打开新的作用域

    …

    b=a;                              //执行默认的operator=,b的内存泄漏了

}                      //作用域借宿,调用b的destructor

String c=a;              //c.data没有定义!因为a.data已被删除!

Copy constructor:

只要程序中有pass-by-value的动作,就会调用copy constructor,例如:

Void doNothing(String localString){}

String s=”The truth is out there”;

doNothing(s);

由于localString是以by value的方式传递,它必须使用缺省的copy constructor,以s为本,进行初始化,因此localString有一个“s所含指针”副本,当doNothing完成任务时,localString退离其scope,于是它的destructor被调用,后果就是:s内含的指针指向一块已被localString删除的内存。(在一个已被删除内存的指针身上再施行delete动作,其结果未定义)

总结:

如果类拥有任何指针,需撰写自己的copy constructor和assignment operator,在这两个函数中将指针所指之数据结构做一份复制品,使每个对象拥有属于自己的一份拷贝。(即深拷贝和浅拷贝问题,就是资源是否也被复制问题)

堆内存并不是唯一需要拷贝构造函数的资源,但它是最常用的一个。打开文件,占有硬件设备服务等也需要深拷贝,它们也是析构函数必须返还的资源类型。因此一个很好的经验是:如果你的类需要一个自定义的析构函数,则它也需要一个拷贝构造函数。

 

6、关于虚拟函数、虚基类及多继承

   可以参考这里:

http://hi.baidu.com/abby_tang/blog/item/4435dbcbd1c74440f21fe7be.html

 


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/kofsky/archive/2008/09/17/2944533.aspx

原创粉丝点击