C++语法杂谈

构造函数、析构函数和虚函数

1. 虚函数机制在构造函数和析构函数中不起作用:在这两个函数中调用虚函数,总是得到虚函数的本地版本;

2. 析构函数自身常常需要设定为虚函数。比如下面的例子。

class base {};
class derived : public base {};

base *p = new derived;
delete p;

上面的例子中,由于p是base类指针,因此类derived的析构函数将不会得到执行。如果类derived中进行了资源分配操作并在析构函数中释放,那就悲剧了。

运算符重载

我们从operator+谈起。比如我们有一个类A:

A A::operator+(const A& a) const {...}

这是比较经典的成员函数重载定义,接受一个引用参数,返回对象A。我们可以在返回的临时对象上做事情:

(a1+a2).do_something();   // a1和a2都是A对象,do_something是普通成员函数。

但这种写法稍微违背直觉(见下文:对临时变量的引用只能是常量引用)。所以建议将operator+的返回值设定为const对象,这样就可以了。

C++编译器默认提供的成员函数

构造函数,析构函数,拷贝构造函数,赋值运算符,地址运算符。

如果你的类里包含指针,小心了。

赋值表达式和侧效

以下表达式行为未定义(?)

A funcA1()
{
    return A();
}

A funcA2()
{
    return A();
}

funcA1() = funcA2();

funcA1和funcA2在VS2013和mingw-gcc4.7.1上有不同的执行顺序。

因此,对于以下写法,最好避免:

A a2 = a1;

上面这条语句在不同的编译环境下可能由于不同的结果:

结果1:使用默认构造函数构造a2,然后通过赋值运算符将a1赋值给a2。

结果2:通过拷贝构造函数从a1构造出a2。

尽管大多数编译器足够智能,不会跑到结果1,但为了我们的理想国度,可以这样写:

A a2(a1);

对临时变量的引用只能是常量引用

const int& i = 1;

const A& a = generate_A_instance();

// generate_A_instance()返回一个class A的实例:
A generate_A_instance
{
    return A();
}

此处必须使用常量引用的原因是强迫编译器为该临时变量分配持续存储空间,非常量引用不具备该能力(类比下const int& i = 1而不能写int& i = 1)。但临时变量本身未必是常量(除非上例中将函数generate_A_instance返回值设定为const A),所以你可以对临时变量调用非常量成员函数。

附注:VS2013允许对generate_A_instance的返回对象进行非常量引用,但这似乎不合逻辑:它不允许对返回内建对象(例如int值)的函数进行非常量引用。

所以建议在尽可能情况下将返回对象的函数设定为const返回,这样就可以了。 虽然这样做会造成不能generate_A_instance().do_something(),但这从逻辑上能讲得通。

 static

当static出现在类内部修饰数据成员,表示的是“所有类对象唯一”,因此在定义时不能再加static修饰:

//.cpp file
// 假设class A有一个静态成员int i
// 这是一个定义,因此一般放在cpp文件内。
// 同样因为这是一个定义,需要再次说明其类型int
// 但不能加上static,
// 否则就会变成“这个变量i仅在该cpp内部有效”的意思。
int A::i = 0;

  

成员指针

对类内的数据对象使用成员指针是合乎语法但没有必要的(?),普通指针语法即可指向类内数据成员;成员指针的主要用途在成员函数上:因为类的成员函数带有隐藏参数this,所以没有办法用一般的函数指针指向类内成员函数。另一方面,也正是由于这个原因,两个不同类中具有相同参数和返回值的函数是不能用同一个指针去指向的。除此以外,成员指针的定义、赋值、调用和普通函数指针是一样的。

class A
{
public:
    int i;
    void func1() { cout << "func1" << endl; }
    void func2() { cout << "func2" << endl; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    A a;
    /***   定义、赋值并使用成员指针   ***/
    // 定义指向类A成员函数的指针,指针变量名pfunc。
    void (A::*pfunc)();
    // 给指针赋值,让它指向A内函数func1
    pfunc = &A::func1;
    // 通过成员指针调用func1
    // 调用时总是绑定到类A的某个实例,
    // 以便编译器传入this指针。
    (a.*pfunc)();    // 注意括号
    // 现在指向func2并调用
    pfunc = &A::func2;
    (a.*pfunc)();
    return 0;
}

成员指针必须和对象绑定,因此即使在类内部定义使用,也必须用this->* 。

const

const表示“一旦定义就不能被改变的量”,除了可代替#define作为编译期常量之外,还可用于如下环境:

const int i = cin.get();

以上,i不是编译期常量,每次执行都可能不一样;但它在定义之后不能改变。

Handle class

尽管C++通过private关键字实现了私有部分在语法上的分离,但修改一个类的private部分仍然可能造成所有使用该类的文件重新编译(比如增加一个private函数)。我们可以仅将公共接口部分放置于头文件中,所有私有部分另起一个class或struct放置在实现文件中,在头文件中通过一个指针(Handle)指向该私有class。

// 引自C++编程思想,1st Edition

// 接口(.h)文件
class handle
{
    struct cheshire;    // 前向声明
    cheshire *smile;   // handle pointer
public:    // 公共接口部分
    void initialize();
};

// 实现(.cpp)文件
// 嵌套结构使用范围分解运算符定义
// class handle的私有部分放在这里
struct handle::cheshire
{
    int i;
};

void handle::initialize()
{
    // 分配私有部分所需存储
    smile = (cheshire *)malloc(sizeof(cheshire));
    smile->i = 0;
}

 友元类

声明友元类可以看做一种前向声明,所以声明友元类时不需要具体的类定义。

另一方面,如果将类的成员函数作为友元,那么类定义是需要的。

class C
{
public:
    void func();

};

class A

{

    friend class B;        // 不需要class B的定义。

    friend void C::func();  // class C的定义是必须的,func必须是public以使得A能够“看到”func()以“认可”它。

};

 

虚函数

1. 如果你在基类中将某个函数声明为纯虚函数,那么该类是不可实例化的;你必须在它的某个子类中给出该虚函数的实际定义。

2. 如果你在基类中将某个函数声明为非纯的虚函数,那么必须提供该函数的定义,否则编译器会报错,因为无法在没有函数定义的情况下构造虚函数表;

综上,虚函数的用法必居其下之一:

1. 在基类中将某个函数声明为纯虚函数,该类不可实例化,在子类中有该函数定义;

2. 在基类中将某个函数声明为非纯的虚函数,且提供该函数定义。

以下是非虚的函数可以做到而虚函数不行的:

在类中仅声明而不定义某个函数,只要你没有调用该函数。