C++ 注意事项
尽量用const 和inline 而不用#define
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
1. 变量中的const
const 可以修鉓值或地址:
int * a3 = &a1; ///non-const 值,non-const 地址
const int * a4 = &a1; ///const 值,non-const 地址
int * const a5 = &a1; ///non-const 值,const 地址
int const * const a6 = &a1; ///const 值,const 地址
const int * const a7 = &a1; ///const 值,const 地址
2. 函数中的const
const 的一些强大的功能基于它在函数声明中的应用。在一个函数声明中,const 可以指的是函数的返回值,或某个参数;对于成员函数,还可以指的是整个函数。
const 成员函数的目的当然是为了指明哪个成员函数可以在const 对象上被调用。但很多人忽视了这样一个事实:仅在const 方面有不同的成员函数可以重载。这是C++的一个重要特性。看这个String 类:
class String {
public:
...
// 用于非const 对象的operator[]
char& operator[](int position)
{ return data[position]; }
// 用于const 对象的operator[]
const char& operator[](int position) const
{ return data[position]; }
private:
char *data;
};
一个成员函数为const 的确切含义是什么? 有两种主要的看法:数据意义上的const(bitwise constness)和概念意义上的const(conceptual constness)。
通过类型转换消除 const 会既有用又安全。这就是:将 一个const 对象传递到一个取非const 参数的函数中,同时你又知道参数不会在函数内部被修改的情况时。
3. C++接口与继承
-
纯虚函数必须在子类中重新声明,但它还是可以在基类中有自己的实现。 纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中重新声明,而且它们在抽象类中往往没有定义。
class JK { public: JK(){} virtual void add()=0; }; void JK::add() { std::cout << "add"; } // class Child: public JK { public: Child(){} virtual void add(); }; void Child::add() { JK::add(); }
- 当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。
- 声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。
- 第一个错误是把所有的函数都声明为非虚函数。这就使得派生类没有特殊化的余地;非虚析构函数尤其会出问题。
- 另一个常见的问题是将所有的函数都声明为虚函数。有时这没错 —- 比如,协议类(Protocol class)就是证据。但是,这样做往往表现了类的设计者缺乏表明坚定立场的勇气。一些函数不能在派生类中重定义,只要是这种情况,就要旗帜鲜明地将它声明为非虚函数。不能让你的函数好象可以为任何人做任何事 —- 只要他们花点时间重新定义所有的函数。
- 决不要重新定义继承而来的非虚函数。
- 决不要重新定义继承而来的缺省参数值。
- 避免 “向下转换” 继承层次,用虚函数。
- 尽可能地使用分层,必须时才使用私有继承,私有继承意味着 “用…来实现”;两个类之间的继承关系为私有,编译器一般不会将派生类对象(如Student)转换成基类对象(如Person);第二个规则是,从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员。
4.模板与继承
类型T 影响类的行为吗?如果T 不影响行为,你可以使用模板。如果T 影响行为,你就需要虚函数,从而要使用继承。模板导致的 “代码膨胀”。
ATL,Active Template Library活动(动态)模板库,是一种微软程序库,支持利用C++语言编写ASP代码以及其它ActiveX程序。通过活动模板库,可以建立COM组件,然后通过ASP页面中的脚本对COM对象进行调用。这种COM组件可以包含属性页、对话框等控件。
WTL 是 Windows Template Library 的缩写,由微软的ATL(Active Template Library) 小组开发,主要是基于 ATL 对Win32API 的封装。从 2.0 后,功能逐步完善,成为了一个完整的支持窗口的框架(windows framework)。 WTL 功能不如MFC完善,但是比 MFC 更小巧,不依赖 MFC 的DLL。
class GenericStack {
public:
GenericStack();
~GenericStack();
void push(void *object);
void * pop();
bool empty() const;
private:
struct StackNode {
void *data; // 节点数据
StackNode *next; // 下一节点
StackNode(void *newData, StackNode *nextNode)
: data(newData), next(nextNode) {}
};
StackNode *top; // 栈顶
GenericStack(const GenericStack& rhs); // 防止拷贝和
GenericStack& // 赋值(参见
operator=(const GenericStack& rhs); // 条款27)
};
class IntStack: private GenericStack {
public:
void push(int *intPtr) { GenericStack::push(intPtr); }
int * pop() { return static_cast<int*>(GenericStack::pop()); }
bool empty() const { return GenericStack::empty(); }
};
class CatStack: private GenericStack {
public:
void push(Cat *catPtr) { GenericStack::push(catPtr); }
Cat * pop() { return static_cast<Cat*>(GenericStack::pop()); }
bool empty() const { return GenericStack::empty(); }
};
因为GenericStack 的成员函数是保护类型,并且接口类把GenericStack 作为私有基类来使用,用户将不可能绕过接口类。因为每个接口类成员函数被(隐式)声明为inline,使用这些类型安全的类时不会带来运行开销;生成的代码就象用户直接使用GenericStack 来编写的一样(假设编译器满足了inline 请求 —- 参见条款33)
5.回调函数、仿函数
Thunk : 将一段机器码对应的字节保存在一个连续内存结构里, 然后将其指针强制转换成函数. 即用作函数来执行,通常用来将对象的成员函数作为回调函数.所谓类成员函数,和对应的全局函数,其实就差一个this指针
回调函数是继承自C语言的。在C++中,应只在与C代码建立接口或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或仿函数(functor),而不是回调函数。
class MyClass
{
pthread_t TID;
void func()
{
//子线程执行代码
}
public:
bool startThread()
{//启动子线程
typedef void* (*FUNC)(void*);//定义FUNC类型是一个指向函数的指针,该函数参数为void*,返回值为void*
FUNC callback = (FUNC)&MyClass::func;//强制转换func()的类型
int ret = pthread_create( &TID , NULL , callback , this );
if( ret != 0 )
return false;
else
return true;
}
};
int main()
{
MyClass a;
a.startThread();
}
仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。有时仿函数的使用是为了函数拥有类的性质,以达到安全传递函数指针,依据函数生成对象,甚至是让函数之间有继承关系,对函数进行运算和操作的效果。比如set就使用了仿函数less ,而less继承的binary_function,就可以看作是对于一类函数的总体声明了,这是函数做不到的。
C语言使用函数指针和回调函数来实现仿函数,例如一个用来排序的函数可以这样使用仿函数
C#是通过委托(delegate)来实现仿函数的。
#include <stdlib.h>
/* Callback function */
int compare_ints_function(void*A,void*B)
{
return*((int*)(A))<*((int*)(B));
}
/* Declaration of C sorting function */
void sort(void*first_item,size_t item_size,void*last_item,int(*cmpfunc)(void*,void*));
int main(void)
{
int items[]={4,3,1,2};
sort((void*)(items),sizeof(int),(void*)(items +3), compare_ints_function);
return 0;
}
在C++里,我们通过在一个类中重载括号运算符的方法使用一个函数对象而不是一个普通函数。
class compare_class{
public:
bool operator()(int A, int B)const{return A < B;}
};
// Declaration of C++ sorting function.
template<class ComparisonFunctor>
void sort_ints(int* begin_items, int num_items, ComparisonFunctor c);
int main(){
int items[]={4, 3, 1, 2};
compare_class functor;
sort_ints(items, sizeof(items)/sizeof(items[0]), functor);
}
Java中的仿函数是通过实现包含单个函数的接口实现的
List<String> list =Arrays.asList("10", "1", "20", "11", "21", "12");
Comparator<String> numStringComparator =new Comparator<String>(){
publicint compare(String o1, String o2){
returnInteger.valueOf(o1).compareTo(Integer.valueOf(o2));
}
};
Collections.sort(list, numStringComparator);
6. 内联函数
尽量用const 和inline 而不用#define
内联函数,它们看起来象函数,运作起来象函数,比宏(macro)要好得多,使用时还不需要承担函数调用的开销。
virtual 的意思是”等到运行时再决定调用哪个函数”
inline 的意思是”在编译期间将调用之处用被调函数来代替”
一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单
7.赋值时,列表初始化效率高
在写构造函数时,必须将参数值传给相应的数据成员。有两种方法来实现。第一种方法是使用成员初始化列表:
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;
}
const成员和引用数据成员只能用列表初始化,不能被赋值。
可以对名字同时声明const 和引用,这样就生成了一个其名字成员在类外可以被修改而在内部是只读的对象。
const string& name; // 必须通过成员初始化列表
-
如果没有为name 指定初始化参数,string 的缺省构造函数会被调用。当在构造函数里对name执行赋值时,会对name 调用operator=函数。这样总共有两次对string 的成员函数的调用:一次是缺省构造函数,另一次是赋值。
- static 类成员永远也不会在类的构造函数初始化。
- 类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初 始化列表中列出的顺序没一点关系。
7. 非局部静态对象初始化:
非局部静态对象:
- 定义在全局或名字空间范围内(例如:theFileSystem 和tempDir)
- 在一个类中被声明为 static,或在一个文件范围被定义为 static。
非局部静态对象初始化依赖顺序的问题很严重。解决方法:
首先,把每个非局部静态对象转移到函数中,声明它为static。
其次,让函数返回这个对象的引用。
这样,用户将通过函数调用来指明对象。换句话说,用函数内部的static 对象取代了非局部静态对象。
这个方法基于这样的事实:虽然关于 “非局部” 静态对象什么时候被初始化,C++几乎没有做过说明;但对于函数中的静态对象(即,”局部” 静态对象)什么时候被初始化,C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。
下面的代码对theFileSystem 和tempDir 都采用了这一技术:
class FileSystem { ... }; // 同前
FileSystem& theFileSystem() // 这个函数代替了
{ // theFileSystem 对象
static FileSystem tfs; // 定义和初始化
// 局部静态对象
// (tfs = "the file system")
return tfs; // 返回它的引用
}
class Directory { ... }; // 同前
Directory::Directory()
{
通过调用 theFileSystem 的成员函数
创建一个 Directory 对象;
//同前,除了 theFileSystem 被theFileSystem()代替;
}
Directory& tempDir() // 这个函数代替了
{ // tempDir 对象
static Directory td; // 定义和初始化
// 局部静态对象
return td; // 返回它的引用
}
8.基数要用虚析构函数
-
当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。
-
当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意
- 实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针
- 基类可以声明为纯虚析构函数,没有开销。
9. 赋值C& C::operator=(const C&);
C++程序员经常犯的一个错误是让operator=返回void,这好象没什么不合 理的,但它妨碍了连续(链式)赋值操作,所以不要这样做。
- 当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用,*this
-
在operator=中对所有数据成员赋值
C& C::operator=(const C& rhs) { // 检查对自己赋值的情况,防止//C a; a=a;//失败 if (this == &rhs) // 假设operator=存在 return *this; ... return *this; }
别名和object identity 的问题不仅仅局限在operator=里。在任何一个用到的函数里都可能会遇到。在用到引用和指针的场合,任何两个兼容类型的对象名称都可能指的是同一个对象。//if (this == &rhs) ,一定要处理。
参考: