这篇文章主要是跟大家聊一下,C++ Data Member 绑定的历史故事,以及对我们现在的代码产生的影响。
extern float x;
class Point3D
{
public:
Point3D(float,float,float);
//问题,被传回和被设定的x 是哪一个 x呢?
float X() const {return x;}
void X(float _newx)const {x=_newx;}
//...
private:
float x,y,z;
};
此段代码中的那个问题,我相信现在大家都能回答的出来吧。 被X()返回的x 毋庸置疑 就是 Point3D内部的那个 x。
不过在早期的 C++编译器上,因为编译器是先见到 外部的x,然后再见到Get 和 Set函数,所以x会被理解成 外部的x,毕竟在看到Point3D内部的x之前,函数已经被解析完成了。
所以当时有一种防御性编程风格:
1 . 所有的data member都放在 class 的首部,以确保正确的绑定:
class Point3D
{
//防御性编程风格
//在class 的首部声明出所有的data member
float x,y,z;
public:
float X() const{return x;}
//....
};
2 . 所有的inline functions,不管大小都放在 class 声明之外:
class Point3D
{
//防御性编程风格
//把所有的 inlines 都移到 class 之外
public:
Point3D();
float X()const;
void X(float _newx)const;
};
inline float Point3D::X()const
{
return x;
}
//...
虽然我们现在可能还能看到这样的风格,不过第2点自从C++2.0之后就没必要了。这个古老的规则被称为”member rewriting rule”。
一个inline函数的函数体,在整个class的声明未被完全看见之前,是不会被解析的。在C++ Standard 中的称呼为 “member scope resolution rules”
所以现代C++对于 inline member function body 的解析会直到整个class 声明出现了才开始,所以在见到class 的右大括号}
之前,不会解析函数体。
不过这对于 member function 的 argument list 并不为真。argument list 中的名称还是会在他们第一次遇到时被解析。因此extern 和 nested type names 之间的非直觉绑定操作还是会发生。
所以下面这段程序中的set 函数的 length 类型最开始都会被解析为 long long 然后当解析到类中定义的 length的时候,才会把之前的解析结果给废除,重新解析。
typedef long long length;
class test
{
public:
void SetVal1(length _val) { printf("%d\t%d\n", sizeof(length), sizeof(val1)); val1 = _val; }
length GetVal1() { printf("%d\t%d\n", sizeof(length), sizeof(val1)); return val1; }
void SetVal2(length _val) { printf("%d\t%d\n", sizeof(length),sizeof(val2)); val2 = _val; }
length GetVal2() { printf("%d\t%d\n", sizeof(length), sizeof(val2)); return val2; }
private:
length val1;
typedef int length;
length val2;
};
int main()
{
test t;
t.SetVal1(0);
t.GetVal1();
printf("%d\n", sizeof(t.GetVal1()));
t.SetVal2(0);
t.GetVal2();
printf("%d\n", sizeof(t.GetVal2()));
return 0;
}
看了上面的讲述,大家对于函数中的sizeof(length)
的结果都很清楚了吧,等价于sizeof(int)
。
不过如果你运行上面的代码的话,你会发现sizeof(val1);
和sizeof(val2)
是不一样的,在遇到类内部定义的length之后,val1的类型并未被更改成int,还是之前的long long。不过这是在Visual Studio 2017上的结果,在gcc 上不予通过编译。
以下为Visual Studio上的运行结果
4 8
4 8
8
4 4
4 4
8
但是这里有一点值得注意的是,如果将上述代码修改为下面这样:
typedef long long length;
class test
{
public:
void SetVal1(length _val);
length GetVal1();
void SetVal2(length _val);
length GetVal2();
private:
length val1;
typedef int length;
length val2;
};
void test::SetVal1(length _val)
{
printf("%d\t%d\n", sizeof(length), sizeof(val1));
val1 = _val;
}
length test::GetVal1()
{
printf("%d\t%d\n", sizeof(length), sizeof(val1));
return val1;
}
void test::SetVal2(length _val)
{
printf("%d\t%d\n", sizeof(length),sizeof(val2));
val2 = _val;
}
length test::GetVal2()
{
printf("%d\t%d\n", sizeof(length), sizeof(val2));
return val2;
}
int main()
{
test t;
t.SetVal1(0);
t.GetVal1();
printf("%d\n", sizeof(t.GetVal1()));
t.SetVal2(0);
t.GetVal2();
printf("%d\n", sizeof(t.GetVal2()));
return 0;
}
编译是无法通过的,因为声明与其定义中的 length 含义是不一致的,所以会编译会报声明与定义不匹配的错误。
所以上述的情况,还是需要防御性编程:请总是把”nested type声明”放在 class的起始处。在上述代码中,如果把 length 的nested定义放在 val1被声明之前,就可以确保绑定的正确性。