C++学习笔记(五) 类杂谈
const成员函数
const成员函数的存在的价值主要在于const对象。我们知道const对象是不可以被修改的,为了保证const对象不能被修改,编译器规定const对象只能调用const修饰的成员函数,它会检查该类成员函数以保证调用此函数不会修改对象的状态。
Const对象只能调用const成员函数,而非const对象则是所有的成员函数都可以调用,但是,有时候,我们也希望const对象存在一个非const的版本。例如定义一个累加器类Accu:
[cpp]
class Accu {
public:
Accu():data(0){}
Accu& add(int d){data += d; return *this;}
const Accu&show() const{cout<<data; return *this;};
private:
int data;
}
可以看出,show函数只是显示结果,所以它是const成员函数。现在有一种编程风格叫做链式编程风格,用上面的类,我们可以如下编写代码:
[cpp]
Accu ac;
ac.add(1).add(30).show();
由于每个函数都返回了对象本身的引用,所以我们可以以链式的风格调用程序,这种风格的好处我就不谈了,现在主要说明它的问题。细心的童鞋可能会发现,show函数只能放在最后被调用,因为它是const成员函数,它返回的是对对象的const引用,也就是说如下写法将形成编译错误:
[cpp]
ac.add(1).add(30).show().add(6);
不能在const对象上调用非const成员函数。
要解决这个问题,我们需要对show函数进行重载,添加一个非const的版本。
[cpp]
Accu&show() {cout<<data; return *this;};
这样子,问题就解决了。这里面其实存在着这样一种函数匹配优先级,那就是非const成员函数比const成员函数的优先级更高。所以重载后,非const对象调用的一定是非const版本的show函数,只有const对象才会调用const版本的show函数。
mutable数据成员
现实中可能有这样的需求,要求即使在const对象中,该成员变量也可以被修改,这时候就可以用mutable关键字对该成员进行修饰,这样即使在从const成员函数中也可以修改该成员的值了。
class 作用域
我们知道,在类的成员函数内部可以直接引用类中定义的成员变量,或是类型等,即使该成员函数是定义在类的外部,实际上,在参数列表中也可以直接引用的,只是返回值类型除外。原因是编译在处理的时候只有遇到函数名时才会决定其作用域,也就是说凡是定义在函数名之后的都可以直接引用类的成员,而定义在它之前的返回值自然是除外的。请看下面这个例子:
[cpp]
class A {
public:
typedef int byte32;
byte32 test(byte32 a);
};
A::byte32 A::test(byte32 a) {
//some code
}
byte32是定义在类A内部的类型,因为返回值处在class作用域之外,所以需要加A::修饰,但参数列表是定义在函数名之后的,所以算是在class作用域之内,因此不需要加A::修饰。
讲到这里,就顺便说一下C++的名称查找机制吧,考虑下面的定义:
[cpp]
typedef string Type;
Type initVal();
class Exercise {
public:
// ...
Type test();
typedef double Type;
Type setVal(Type);
Type initVal();
private:
int val;
};
Type Exercise::setVal(Type parm) {
val = parm + initVal();
}
你能清楚的说出这里面的Type都分别是在哪里定义的吗?
C++的名称查找机制是这样的,如果查找的是类型名,那么首先在该类型使用的函数(或block)里寻找,如果找不到,再到它所在的类中该函数定义之前的部分去寻找,如果,仍然找不到,则到该类的定义之前去寻找(如果该成员函数定义在类外,则是到该成员函数的定义之前去寻找);如果查找的是变量名或函数名,其区别在于,当查找class作用域时是搜索整个class而不仅仅是该变量的使用之前的部分。
所以,我们再回头看一下代码,class上面的Type initVal()的返回类型自然就是哪个全局Type了,而在类中的Type initVal()中的Type是什么呢。因为这个Type不是在函数内部使用的,所以省去了查找函数作用域的一步,它直接查找类作用域,这样就找到了在类中定义的Type。setVal函数的声明和这是一样的,都是类中定义的Type,关键看它的定义,我们前面说过,成员函数的参数列表是在class作用域中的,所以参数列表中Type是类中定义的Type,但返回值就不一样了,它是那个全局Type。现在看该函数的内部,对于变量parm,编译器首先搜索函数内部,发现了参数列表上的parma,就是它了。但是val和initVal在函数内部都木有声明,所以,下一步就是搜索整个class作用域,发现了它们的声明位置。在类中还有一个函数声明Type test();由于它的声明放在类中Type的定义之前,我们前面说过,编译对于类型名在class作用域中的查找时只查找它在使用的地方之前的部分,所以这里它是找不到Type的定义的,那么接下来编译器就会去搜查类定义之外的环境,从而找到全局的Type定义。
嗯,讲的似乎有点乱,这里。本来不想写这一部分的,因为在实际的开发中命名重复是需要尽量避免的,所以这一部分实际上是理论意义大于实践意义,为了笔记的完善,我还是把它写上了,如果看不太明白就直接跳过吧。
初始化列表
在类的构造函数里,C++为我们提供了一个机制,用于对类成员进行初始,这就是初始化列表。考虑下面构造函数的两种定义方式:
[cpp]
A(B& pb) {
<span style="white-space:pre"> </span>b = pb;
}
A(B& pb): b(pb) {
}
在第二种方式里,冒号后面的其实就是初始化列表,当这部分省略时,编译器会使用默认的构造函数为每个类成员生成初始化语句,所以第一种方式就相当于:
[cpp]
A(B& pb):b() {
<span style="white-space:pre"> </span>b = pb;
}
考虑这两种方式,如果类型B是原生数据类型,二者之间实际上是没有什么区别的,但如果是类类型,就得考虑二者之间的性能开销问题了,第一种方式首先调用默认的构造函数,然后再进行赋值,第二种方式则直接调用拷贝构造函数,假设这两个构造函数定义如下:
[cpp]
B():C(0) {
}
B(B& b):C(b.c) {
}
可以看出在这种情况下,默认构造函数和拷贝构造函数的性能是一样的,但第一种方式多了赋值的开销,如果说这种方式,它们的性能开销差异还不够名显的话,那么再考虑下面的情况:
[cpp]
A(C& c):b() {
<span style="white-space:pre"> </span>b = B(c);
}
A(C& c):b(c) {
}
这次,底一种是先调用了默认的构造函数,然后再调用了一个非默认构造函数,接着再调用了一个=操作(这里可以重载),而第二种方式则只需要调用一次非默认的构造函数就可以了。
所以,总结,在一些情况下,使用初始化列表和在构造函数体里面进行初始化,它们的性能是没有多少差异的,但在另一些情况下,使用初始化列表将具有更高的效率。这是从效率上来说的,实际上,在某些情况下,如成员变量并没有提供默认构造函数时,这就要求我们必须要使用初始化列表了。所以,当二者都处于备选时,初始化列表往往是我们更优的选择,但这只是一种建议,而非强制的要求,具体情况还得看我们的业务逻辑。
作者:jus易做图anda
补充:软件开发 , C++ ,