千变万化-江西大学生门户's Archiver

admin 发表于 2006-8-22 16:10

Effective C

Effective C   资料整理   (2)                                       作者:(ECNU)孟庆涛
8:有关Const的相关知识
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const,还有,两者都不指定为const:
char *p              = "hello";          // 非const指针, 非const数据const char *p        = "hello";          // 非const指针, const数据char * const p       = "hello";              // const指针, 非const数据const char * const p = "hello";              // const指针, const数据
语法并非看起来那么变化多端。一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。
在指针所指为常量的情况下,有些程序员喜欢把const放在类型名之前,有些程序员则喜欢把const放在类型名之后、星号之前。所以,下面的函数取的是同种参数类型:
class widget { ... };
void f1(const widget *pw);      // f1取的是指向 widget常量对象的指针void f2(widget const *pw);      // 同f2
因为两种表示形式在实际代码中都存在,所以要使自己对这两种形式都习惯。
const的一些强大的功能基于它在函数声明中的应用。在一个函数声明中,const可以指的是函数的返回值,或某个参数;对于成员函数,还可以指的是整个函数。
让函数返回一个常量值经常可以在不降低安全性和效率的情况下减少用户出错的几率。例如,看这个有理数的operator*函数的声明:
const rational operator*(const rational& lhs,const rational& rhs);
很多程序员第一眼看到它会纳闷:为什么operator*的返回结果是一个const对象?因为如果不是这样,用户就可以做下面这样的坏事:
rational a, b, c;
...
(a * b) = c;      // 对a*b的结果赋值
我不知道为什么有些程序员会想到对两个数的运算结果直接赋值,但我却知道:如果a,b和c是固定类型,这样做显然是不合法的。一个好的用户自定义类型的特征是,它会避免那种没道理的与固定类型不兼容的行为。对我来说,对两个数的运算结果赋值是非常没道理的。声明operator*的返回值为const可以防止这种情况,所以这样做才是正确的。
还有一种情况下,通过类型转换消除const会既有用又安全。这就是:将一个const对象传递到一个取非const参数的函数中,同时你又知道参数不会在函数内部被修改的情况时。第二个条件很重要,因为对一个只会被读的对象(不会被写)消除const永远是安全的,即使那个对象最初曾被定义为const。
例如,已经知道有些库不正确地声明了象下面这样的strlen函数:
size_t strlen(char *s);
strlen当然不会去修改s所指的数据――至少我一辈子没看见过。但因为有了这个声明,对一个const char *类型的指针调用这个函数时就会不合法。为解决这个问题,可以在给strlen传参数时安全地把这个指针的const强制转换掉:
const char *klingongreeting = "nuqneh"; // "nuqneh"即"hello"
size_t length =strlen(const_cast<char*>(klingongreeting));
但不要滥用这个方法。只有在被调用的函数(比如本例中的strlen)不会修改它的参数所指的数据时,才能保证它可以正常工作。
我下面说一下有关const指针的用法(这一部分大家可参考C  Primer(3))
当我们写下下面的代码
Const double value=3.0;
Double *ptr=&value;   (error)
编译器会报错,因为它把“试图通过该指针间接地改变对象值”的动作标记为错误。但是这并不意味着我们不能间接的指向一个const对象,只标明我们必须声明一个指向常量的指针来做这件事情。例如
Const double *cptr=&value;  ( right)
此时,类似于*cptr=3.8等的改变原值的附值操作是错的,因为我们不能改变常量指针所指对象的值。
但是,常量指针也可以指向一个非常量对象,例如
Double  meng=23.3;    cptr=&meng;   (right)
虽然meng 不是常量,但是,试图通过cptr改变它的值仍然会导致编译错误。
在实际的程序中,指向const 的指针经常用来做函数参数,它作为一个月定:被传给函数的实际对象在函数种不能被修改。例如
Int strcmp(const char *str1,const char *str2);
这样的好处是,我们可以把常量或者非常量传递给此函数,而且次函数保证不会对它做修改。
9:尽量使用初始化而不要在构造函数里赋值
看这样一个模板,它生成的类使得一个名字和一个t类型的对象的指针关联起来。
template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...
private:
  string name;
  t *ptr;
};
(因为有指针成员的对象在进行拷贝和赋值操作时可能会引起指针混乱,namedptr也必须实现这些函数)
在写namedptr构造函数时,必须将参数值传给相应的数据成员。有两种方法来实现。第一种方法是使用成员初始化列表:
template<class t>
namedptr<t>::namedptr(const string& initname, t *initptr  )
: name(initname), ptr(initptr)
{}
第二种方法是在构造函数体内赋值:
template<class t>
namedptr<t>::namedptr(const string& initname, t *initptr)
{
  name = initname;
  ptr = initptr;
}
两种方法有重大的不同。
(1)从纯实际应用的角度来看,
从纯实际应用的角度来看,有些情况下必须用初始化。特别是const和引用数据成员只能用初始化,不能被赋值。所以,如果想让namedptr<t>对象不能改变它的名字或指针成员,就建议声明成员为const:
template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...
private:
  const string name;
  t * const ptr;
};
这个类的定义要求使用一个成员初始化列表,因为const成员只能被初始化,不能被赋值。
如果namedptr<t>对象包含一个现有名字的引用,情况会非常不同。但还是要在构造函数的初始化列表里对引用进行初始化。还可以对名字同时声明const和引用,这样就生成了一个其名字成员在类外可以被修改而在内部是只读的对象。
template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...
private:
  const string& name;               // 必须通过成员初始化列表
                                    // 进行初始化
  t * const ptr;                    // 必须通过成员初始化列表
                                    // 进行初始化
};
然而前面最初的类模板不包含const和引用成员。即使这样,用成员初始化列表还是比在构造函数里赋值要好。
(2)这次的原因在于效率。
当使用成员初始化列表时,只有一个string成员函数被调用。而在构造函数里赋值时,将有两个被调用。为了理解为什么,请看在声明namedptr<t>对象时都发生了些什么。
对象的创建分两步:
1. 数据成员初始化。
2. 执行被调用构造函数体内的动作。
(对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前)
对namedptr类来说,这意味着string对象name的构造函数总是在程序执行到namedptr的构造函数体之前就已经被调用了。问题只在于:string的哪个构造函数会被调用?
这取决于namedptr类的成员初始化列表。如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。
相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化。

页: [1]
※ 本 站 声 明※

点击注册 千变万化是由昌大师生建立的非官方南昌大学论坛,言论纯属发表者个人意见,与本论坛立场无关
如果?容有涉及侵权,请马上联络
管理员 有事请留言

sitemap

Powered by Discuz! Archiver 6.1.0  © 2001-2007 Comsenz Inc.