C++程序员应了解的那些事(36)Effective STL第6条:当心C++编译器中最烦人的分析机制 --- 调用构造函数被误认为是函数声明的问题

<示例代码导入-1>
#include <iostream>
using namespace std;

struct Calculate
{
    Calculate(int& i);
    int value;
};
struct Gadget
{
    Gadget(Calculate& clc);
    Calculate clc;
};
Calculate::Calculate(int& i)
{
    i = i*100;
    std::cout << i << std::endl; 
}
Gadget::Gadget(Calculate& clc):clc(clc){}

void fun(int& i)
{
    //warning:parentheses were disambiguated as a function declaration 
    Gadget g(Calculate(i));//变量定义?or 函数声明?
}

int main()
{
    int i = 3;
    fun(i);
    std::cout << i << endl;
}

        上述代码运行时:Gadget g(Calculate(i))被解释为一个函数声明,构造函数Calculate::Calculate(int& i)并没有被调用,所以main()中打印 i =3。

         修改为Gadget g{Calculate(i)}后,可以保证该条语句被解释为调用构造函数声明一个变量。

********************************* 问题分析 ***********************************

C++是较为底层的面相对象语言,在底层的语法规则分析中,有很多隐藏的分析机制。
(1)C++中的普遍规律,即尽可能地解释为函数声明。
(2)把形式参数的声明用括号括起来是非法的,但给函数参数加上括号却是合法的,所以通过增加一对括号,我们强迫编译器按我们的方式来工作。

<演示说明①>
假设我们有一个存放整型(int)的文件,你想把这些整数拷贝到一个list中,那么你可能会使用下面的做法:

std::ifstream dataFile("ints.dat");
//使用list的区间构造函数来初始化list(C++编译器可能会识别错误)
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>()); 

        这种做法的思路是, 把一对std::istream_iterator传入std::list的区间构造函数中, 从而把文件的整数复制到std::list中去。

        这段代码可以通过编译, 但是运行时它什么也不会做, 它不会从文件中读去任何数据, 不会创建std::list, 这是因为第二条语句并没有定义一个std::list对象, 也没有调用构造函数。

       这段代码的本义:创 建一个std::list< int >对象data, 构造该对象的方法是: 把一对std::istream_iterator传入std::list< int >的区间构造函数中, 从而把文件中的整数复制到std::list< int >中去。

       但是编译器其实是这么理解的:这里声明了一个函数data(), 其返回值为std::list< int >, 这个_Data()函数有如下两个参数:第一个参数的名称是dataFile,,它的类型是std::istream_iterator< int >, dataFile两边的括号是多余的, 会被忽略。第二个参数没有名称,它的类型是一个“不带任何参数并返回一个std::istream_iterator< int >的函数”的指针。

      蛋碎!!!!!!但是这个解释却与C++中的一条普通规律相符: 尽可能的解释为函数声明。

  • C++编译器可能会上面list的构造函数理解为一种函数:包含两个参数和一个list<int>返回值。
  • 因为编译器可能会把上面那种形式的构造函数理解为一种函数,因此上面的data不会做任何事情,因此data为空。

<演示说明②>

  • 下面的演示案例也介绍了C++的分析机制可能会把构造函数误认为函数!!!
  • 编译器可能会把下面的对象构造语句误认为:函数名为w,无参数,返回值为Widget类型的函数。
class Widget
{
    //假设Widget使用默认构造函数
};
int main()
{
    //我们原本是想使用Widget的默认构造函数,但是可能会被误认为函数
    Widget w();
}

<解决办法①>

  • 多增加一对括号来跳过编译器的分析机制!(将形参的声明用括号括起来是非法的,但给函数参数加上括号却是合法的
  • 下面对list构造函数的第一个参数增加了一对括号:
std::ifstream dataFile("ints.dat");
list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>()); 

        不同编译器的分析机制不同,不同的编译器对此种解析办法的分析可能会不一致,因此你还需要下面的解决办法②。

<解决办法②>根据上面的代码,我们可以为istream_interator迭代器取名(而不是使用匿名)

std::ifstream dataFile("ints.dat");
 
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

说明:

<1>实际编程中都建议使用匿名的istream_interator对象,但是此处为了跳过编译器的分析机制,这种代价是值得的。

<2>使用临时命名迭代器来分步完成需求,虽然这样做与标准STL使用有点违背了,但是为了没有二义性和提高代码可读性和方便维护是比较提倡的。

<解决办法③> ※使用大括号“{}”初始化
现在C++标准已经提供了使用大括号“{}”来初始化,因此上面的所有问题都可以得到解决。

<注>:C++语法分析机制
下面这行代码声明了一个带double参数并返回int的函数:
int _Func( double _InDouble );
下面这行代码做了同样的事情,参数_InDouble两边的括号是多余的, 会被忽略:
int _Func( double ( _InDouble ) );
下面这行代码声明了同样的函数, 只是它省略了参数名称:
int _Func( double );

再来看三个函数声明, 第一个声明了一个函数_Fuck(), 他的参数是一个不接收任何参数并返回一个double类型的函数的指针:
int _Fuck( double( *_ptrFunc ) () );
有另外一种方式可以表明同样的意思, 唯一的区别是, _ptrFunc用非指针的形式来声明(这种形式在C语言和C++中都有效):
int _Fuck( double _ptrFunc() );
跟通常一样, 参数名称可以忽略, 因此下面是_Fuck()的第三种声明, 其中参数名_ptrFunc被省略了:
int _Fuck( double () );

☆☆请注意围绕参数名的括号(比如_Func()的第二个声明中的_InDouble)与独立的括号的区别, 围绕参数名的括号被忽略, 而独立的括号则表明参数列表的存在: 它们说明存在一个函数指针的参数。

参考链接:

http://doc.okbase.net/bephax/archive/21214.html

https://cloud.tencent.com/developer/ask/43679

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值