本文将简要的讨论以下几个问题
- 什么是Sequence Point
- C++的构造函数为什么没有返回值
- 如何显示调用构造函数和析构函数
- 拷贝构造函数为什么必须是引用传递
- 类成员函数中static和const 不能连用的原因
1. 什么是Sequence Point
在现今的笔试中 ,我们经常会遇到如下的问题,当下列语句执行完后,a的值是多少?
int a = 1; // no side effect
a = a ++; // side effect
答案无非是提供了,1, 2, undefined…
其实上述例子,在C++中已经有相应的名词Sequence Point。
A sequence point is a point in the program’s exexution sequence where all previous side-effects shall have take place and where all subsequent side-effects shall not have take place.
在C++中,表达式计算存在两种类型,一是无副作用的,而是有副作用(side effect)的。 如上述代码所示。
而一个 sequence point 按照上述定义为,该点前的表达式的所有副作用,在程序执行到达改点之前发生完毕;该点后的表达式的所有副作用,在程序执行到达该点时尚未发生。
- 法则1 在表达式求值时,在前一个和下一个顺序点之中,一个对象所存储的值至多只能被修改一次。如下图的结果是undefined
- 法则2 在表达式求值的过程中会更改某个对象的值,要求更改前的值被读取的唯一目的是用来确定存入新值。
在C中,规定的Sequence Point很少,因为这有益于编译器的优化最大化。
2. C++的构造函数为什么没有返回值
上一个简单的例子,帮助理解与记忆。
class Base {
// ...
};
void foo(int a) {
// do something...
}
void foo(const Base& base) {
// do something...
}
int main()
{
// 如果构造函数返回值,下面的结果将调用哪个函数?
foo(Base());
return 0;
}
很显然,构造函数不设定返回值,是因为构造函数的特殊性质决定的。如果有返回值,那么将会存在很多问题。
3. 如何显示调用构造函数和析构函数
事实上,我们采用new操作符时,一般会发生以下三件事情:
- 调用::operator new 分配所需内存
- 调用对象的构造函数
- 返回新分配的并构造的对象的指针
我们用以下代码来模拟这个过程。结果说明,我们总是可是显示的调用构造函数和析构函数。
class Base
{
public:
Base() { cout << "constructors" << endl; }
~Base() { cout << "Destructors" << endl; }
};
int main()
{
Base *pb = (Base *)malloc(sizeof(Base));
new(pb) Base();
pb->~Base();
delete pb;
return 0;
}
4. 拷贝构造函数为什么必须是引用传递
在深入探讨之前,首先列出拷贝初始化的几种情况:
- 在使用=定义变量时, 如 Foo newObj = oldObj;
- 将 一个对象作为实参传递给一个非引用类型的实参;
- 从一个返回类型为非引用类型的函数返回一个对象;
- 用花括号初始化一个数组中的元素或一个聚合类中的成员;
- 某些类类型会对他们所分配的对象使用拷贝初始化。例如,当我们初始化标准库容器或者调用insert或push成员,容器会对其进行拷贝初始化。而用emplace成员创建的元素则都进行直接初始化。
class Base
{
public:
Base() { cout << "constructors" << endl; }
Base(const Base bs) {
a = bs.num;
}
~Base() { cout << "Destructors" << endl; }
private:
int num;
};
正如上述所说的,在函数调用过程中,具有非引用类型的参数进行拷贝初始化。类似的,当一个具有非引用类型的返回类型时,返回值会被用来初始化调用方的结果。
拷贝构造函数被用来初始化非引用类类型参数,如果参数不是引用类型,则将进入死循环–>因为为了调用拷贝构造函数,必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。
5. 类成员函数中static和const 不能连用的原因
C++的设计准则之一:nonstatic member function必须至少和nonmember function有相同的效率。
例如:
float magnitude3d(const Point3d *_this) { //... }
float Point3d::magnitude3d() const { //... }
对于后者的nonstatic member function而言,其最终会扩展为:
Point3d Point3d::magnitude(const Point3d *const this) {
//...
}
由上可见,const修饰的是指针this,而this属于object的范畴。而对于static而言,类中的static成员函数或成员变量,不隶属于某个具体的object,其范畴是整个class。因此,修饰的范畴不同,故static和const在类中对同一个成员函数或者成员变量不能一起使用。