讨论记录之C++细节

ParticipantsLFHZPCPPZY

Date 08-09-16   7:20PM

Recorder: CPPZY

参考文献:

1、《effective C++2nd EditionScott Meyers etc.

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

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

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

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

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

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

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

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

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

5、构造函数和析构函数

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

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

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

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

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

移位五,关系六;

等于不等排第七;

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

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

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

逗号排最后。

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

 

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

见两个例子:

(1) int x=1y=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>bab;加括号。此语句有三个运算符:=>、? :,应该怎样加括号呢?

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

 

2、尽量以constinline取代#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; }

 

3const的作用

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关键字。

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

  1. class Base  
  2. {
  3. public:
  4.  Base();
  5.  virtual ~Base();
  6.  public:
  7.  virtual void f(float x)
  8.  {
  9.   cout << "Base f(float)" << x <<endl;
  10.  }
  11.  void g(float x)
  12.  {
  13.   cout<< "Base g(float)" << x <<endl;
  14.  }
  15.  void h(float x)
  16.  {
  17.   cout<< "Base h(float)" << x <<endl;
  18.  }
  19. };
  20. 派生类:
  21. class Derived:public Base
  22. {
  23. public:
  24.  Derived();
  25.  virtual ~Derived();
  26. public:
  27.  virtual void f(float x)
  28.  {
  29.   cout << "Derived f(float)" << x <<endl;
  30.  }
  31.  void g(int x)
  32.  {
  33.   cout<< "Derived g(int)" << x <<endl;
  34.  }
  35.  void h(float x)
  36.  {
  37.   cout<< "Derived h(float)" << x <<endl;
  38.  }
  39. };
  40.  Derived  d;
  41.  Base *pb = &d;
  42.  Derived *pd = &d;
  43.  pb->f(3.14f); // Derived f(float)3.14 调用派生类函数
  44.  pd->f(3.14f); // Derived f(float)3.14 调用派生类函数
  45.  pb->g(3.14f); // Base g(float)3.14 (!) 调用基类函数
  46.  pd->g(3.14f); // Derived g(int)3       调用派生类函数  
  47.  pb->h(3.14f); // Base h(float)3.14 (!) 调用基类函数
  48.  pd->h(3.14f); // Derived h(float)3.14  调用派生类函数

总结:

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

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

 

5、构造函数和析构函数

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

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

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

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

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

(4)            如果类内动态分配内存,需为此类声明一个copy constructorassignment运算符,请看图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运算符对着amenmbers执行一个member一个member的逐次赋值动作,将内容赋给bmembers。对指针(a.datab.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问题,第二,ab内含的指针如今指向相同的字符串,因此,当其中一个离开生存空间时,其destructor会删除内存,而此内存目前仍被另一个指针所指,例如:

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

{

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

   

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

}                      //作用域借宿,调用bdestructor

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 constructorassignment operator,在这两个函数中将指针所指之数据结构做一份复制品,使每个对象拥有属于自己的一份拷贝。(即深拷贝和浅拷贝问题,就是资源是否也被复制问题)

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

 

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

   可以参考这里:

http://hi.baidu.com/abby_tang/blog/item/4435dbcbd 1c 74440f 21fe7be.html

 

全文由 ZY 完成

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值