杂项技术问题

杂项技术问题

什么是函数对象?

当然是一个在某种程度上表现得像函数的对象。通常,这意味着一个定义了应用运算符(`operator()`)的类的对象。

函数对象比函数更通用,因为函数对象可以拥有在多次调用中持续存在的状态(如静态局部变量),并且可以从对象外部初始化和检查(与静态局部变量不同)。例如

    class Sum {
        int val;
    public:
        Sum(int i) :val(i) { }
        operator int() const { return val; }        // extract value

        int operator()(int i) { return val+=i; }    // application
    };

    void f(vector<int> v)
    {
        Sum s = 0;  // initial value 0
        s = for_each(v.begin(), v.end(), s);    // gather the sum of all elements
        cout << "the sum is " << s << "\n";

        // or even:
        cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "\n";
    }

请注意,带有内联应用运算符的函数对象可以很好地内联,因为没有可能混淆优化器的指针。相比之下:当前的优化器很少(从不?)能够内联通过函数指针的调用。

函数对象在标准库中广泛用于提供灵活性。

如何将值(例如数字)转换为 `std::string`?

调用 `to_string`

对于该答案未涵盖的高级和特殊情况用法,请继续阅读……

有两种简单的方法可以做到这一点:你可以使用 `` 工具或 `` 库。一般来说,你应该优先使用 `` 库

`` 库允许你使用以下语法将几乎任何内容转换为 `std::string`(示例将 `double` 转换为字符串,但你可以替换任何可以使用 `<<` 运算符打印的内容)

// File: convert.h
#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>

class BadConversion : public std::runtime_error {
public:
  BadConversion(const std::string& s)
    : std::runtime_error(s)
    { }
};

inline std::string stringify(double x)
{
  std::ostringstream o;
  if (!(o << x))
    throw BadConversion("stringify(double)");
  return o.str();
}

`std::ostringstream` 对象 `o` 提供了与 `std::cout` 相同的格式化功能。你可以使用操纵符和格式标志来控制结果的格式,就像对其他 `std::cout` 一样。

在此示例中,我们通过重载的插入运算符 `<<` 将 `x` 插入到 `o` 中。这会调用 iostream 格式化功能将 `x` 转换为 `std::string`。`if` 测试确保转换正常进行——对于内置/固有类型,它应该总是成功,但 `if` 测试是一种好的风格。

表达式 `o.str()` 返回包含已插入流 `o` 中的任何内容的 `std::string`,在此例中是 `x` 的字符串值。

以下是使用 `stringify()` 函数的方法

#include "convert.h"

void myCode()
{
  double x = /*...*/ ;
  // ...
  std::string s = "the value is " + stringify(x);
  // ...
}

如何将 `std::string` 转换为数字?

调用 `stoi`

对于该答案未涵盖的高级和特殊情况用法,请继续阅读……

有两种简单的方法可以做到这一点:你可以使用 `` 工具或 `` 库。一般来说,你应该优先使用 `` 库

`` 库允许你使用以下语法将 `std::string` 转换为几乎任何内容(示例将 `double` 转换为字符串,但你可以替换任何可以使用 `>>` 运算符读取的内容)

// File: convert.h
#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>

class BadConversion : public std::runtime_error {
public:
  BadConversion(const std::string& s)
    : std::runtime_error(s)
    { }
};

inline double convertToDouble(const std::string& s)
{
  std::istringstream i(s);
  double x;
  if (!(i >> x))
    throw BadConversion("convertToDouble(\"" + s + "\")");
  return x;
}

`std::istringstream` 对象 `i` 提供了与 `std::cin` 相同的格式化功能。你可以使用操纵符和格式标志来控制结果的格式,就像对其他 `std::cin` 一样。

在此示例中,我们初始化 `std::istringstream` `i` 并传入 `std::string` `s`(例如,`s` 可能是字符串 `"123.456"`),然后我们通过重载的提取运算符 `>>` 将 `i` 提取到 `x` 中。这会调用 iostream 格式化功能,根据 `x` 的类型尽可能多地/适当地转换字符串。

`if` 测试确保转换正常进行。例如,如果字符串包含不适合 `x` 类型的字符,`if` 测试将失败。

以下是使用 `convertToDouble()` 函数的方法

#include "convert.h"

void myCode()
{
  std::string s = /*...a string representation of a number...*/ ;
  // ...
  double x = convertToDouble(s);
  // ...
}

你可能希望增强 `convertToDouble()`,以便它可选地检查是否有任何剩余字符

inline double convertToDouble(const std::string& s,
                              bool failIfLeftoverChars = true)
{
  std::istringstream i(s);
  double x;
  char c;
  if (!(i >> x) || (failIfLeftoverChars && i.get(c)))
    throw BadConversion("convertToDouble(\"" + s + "\")");
  return x;
}

我可以将上述函数模板化,以便它们与其他类型一起使用吗?

可以——适用于支持 `iostream` 风格输入/输出的任何类型。

例如,假设你想将 `Foo` 类的一个对象转换为 `std::string`,或者反过来:从 `std::string` 转换为 `Foo`。你可以根据前面常见问题解答中所示的函数编写一系列转换函数,或者你可以编写一个模板函数,让编译器完成繁重的工作。

例如,要将任意类型 `T` 转换为 `std::string`,只要 `T` 支持 `std::cout << x` 这样的语法,你就可以使用这个

// File: convert.h
#include <iostream>
#include <sstream>
#include <string>
#include <typeinfo>
#include <stdexcept>

class BadConversion : public std::runtime_error {
public:
  BadConversion(const std::string& s)
    : std::runtime_error(s)
    { }
};

template<typename T>
inline std::string stringify(const T& x)
{
  std::ostringstream o;
  if (!(o << x))
    throw BadConversion(std::string("stringify(")
                        + typeid(x).name() + ")");
  return o.str();
}

以下是使用 `stringify()` 函数的方法

#include "convert.h"

void myCode()
{
  Foo x;
  // ...
  std::string s = "this is a Foo: " + stringify(x);
  // ...
}

你也可以通过将此添加到文件 `convert.h` 来从支持 `iostream` 输入的任何类型进行转换

template<typename T>
inline void convert(const std::string& s, T& x,
                    bool failIfLeftoverChars = true)
{
  std::istringstream i(s);
  char c;
  if (!(i >> x) || (failIfLeftoverChars && i.get(c)))
    throw BadConversion(s);
}

以下是使用 `convert()` 函数的方法

#include "convert.h"

void myCode()
{
  std::string s = /*...a string representation of a Foo...*/ ;
  // ...
  Foo x;
  convert(s, x);
  // ...
  // ...code that uses x...
}

为了简化你的代码,特别是对于轻量级、易于复制的类型,你可能希望在文件 `convert.h` 中添加一个按值返回的转换函数

template<typename T>
inline T convertTo(const std::string& s,
                   bool failIfLeftoverChars = true)
{
  T x;
  convert(s, x, failIfLeftoverChars);
  return x;
}

这会简化你的一些“使用”代码。你通过显式指定模板参数 `T` 来调用它

#include "convert.h"

void myCode()
{
  std::string a = /*...string representation of an int...*/ ;
  std::string b = /*...string representation of an int...*/ ;
  // ...
  if (convertTo<int>(a) < convertTo<int>(b))
    /*...*/ ;
}

为什么我的编译时间这么长?

你的编译器可能有问题。它可能很旧,你可能安装错误,或者你的电脑可能很老旧。我无法帮助你解决这些问题。

然而,更可能的是,你尝试编译的程序设计不佳,导致编译它涉及到编译器检查数百个头文件和数万行代码。原则上,这是可以避免的。如果这个问题是你的库供应商的设计问题,你无能为力(除了改用更好的库/供应商),但你可以组织自己的代码以在更改后最大限度地减少重新编译。这样设计的代码通常会更好、更易于维护,因为它们表现出更好的关注点分离。

考虑一个面向对象程序的经典示例

    class Shape {
    public:     // interface to users of Shapes
        virtual void draw() const;
        virtual void rotate(int degrees);
        // ...
    protected:  // common data (for implementers of Shapes)
        Point center;
        Color col;
        // ...
    };

    class Circle : public Shape {
    public: 
        void draw() const;
        void rotate(int) { }
        // ...
    protected:
        int radius;
        // ...
    };

    class Triangle : public Shape {
    public: 
        void draw() const;
        void rotate(int);
        // ...
    protected:
        Point a, b, c;
        // ...
    };  

这个想法是用户通过 `Shape` 的公共接口操作形状,而派生类(如 `Circle` 和 `Triangle`)的实现者共享由保护成员表示的实现方面。

这个看似简单的想法存在三个严重问题

  • 定义对所有派生类都有帮助的共享实现方面并不容易。因此,保护成员集可能需要比公共接口更频繁地更改。例如,即使“中心”对于所有 `Shape` 来说都是一个有效概念,但为 `Triangle` 维护一个点“中心”是很麻烦的——对于三角形,只在有人感兴趣时才计算中心更有意义。
  • 保护成员可能依赖于 `Shape` 用户宁愿不必依赖的“实现”细节。例如,大部分(大部分?)使用 `Shape` 的代码在逻辑上独立于“颜色”的定义,但 `Shape` 定义中存在 `Color` 可能需要编译定义操作系统颜色概念的头文件。
  • 当保护部分发生变化时,`Shape` 的用户必须重新编译——即使只有派生类的实现者才能访问保护成员。

因此,在基类中存在“对实现者有帮助的信息”,同时该基类又充当用户接口,这是实现不稳定、用户代码虚假重新编译(当实现信息更改时)以及用户代码中过多包含头文件(因为“对实现者有帮助的信息”需要这些头文件)的根源。这有时被称为“脆性基类问题”。

显而易见的解决方案是为用作用户接口的类省略“对实现者有帮助的信息”。也就是说,将接口纯粹化。也就是说,将接口表示为抽象类

    class Shape {
    public:     // interface to users of Shapes
        virtual void draw() const = 0;
        virtual void rotate(int degrees) = 0;
        virtual Point center() const = 0;
        // ...

        // no data
    };

    class Circle : public Shape {
    public: 
        void draw() const;
        void rotate(int) { }
        Point center() const { return cent; }
        // ...
    protected:
        Point cent;
        Color col;
        int radius;
        // ...
    };

    class Triangle : public Shape {
    public: 
        void draw() const;
        void rotate(int);
        Point center() const;
        // ...
    protected:
        Color col;
        Point a, b, c;
        // ...
    };  

用户现在与派生类实现的更改隔绝。我见过这种技术将构建时间缩短几个数量级。

但是,如果真的有一些信息是所有派生类(或仅仅是几个派生类)共有的呢?只需将这些信息作为一个类,并让实现类也从它派生

    class Shape {
    public:     // interface to users of Shapes
        virtual void draw() const = 0;
        virtual void rotate(int degrees) = 0;
        virtual Point center() const = 0;
        // ...

        // no data
    };

    struct Common {
        Color col;
        // ...
    };

    class Circle : public Shape, protected Common {
    public: 
        void draw() const;
        void rotate(int) { }
        Point center() const { return cent; }
        // ...
    protected:
        Point cent;
        int radius;
    };

    class Triangle : public Shape, protected Common {
    public: 
        void draw() const;
        void rotate(int);
        Point center() const;
        // ...
    protected:
        Point a, b, c;
    };  

包含 `if` 的宏应该如何处理?

理想情况下,你会摆脱宏。宏以四种不同的方式邪恶邪恶#1邪恶#2邪恶#3邪恶#4,无论它们是否包含 `if`(但如果它们包含 `if`,它们尤其邪恶)。

尽管如此,即使宏是邪恶的,有时它们是其他邪恶中较小的一个。当这种情况发生时,请阅读本常见问题,以便你知道如何让它们“不那么糟糕”,然后捏着鼻子做实用。

这是一个天真的解决方案

#define MYMACRO(a,b) \                (Bad)
    if (xyzzy) asdf()

如果有人在 `if` 语句中使用该宏,这将导致大问题

if (whatever)
    MYMACRO(foo,bar);
else
    baz;

问题在于 `else baz` 与错误的 `if` 嵌套:编译器看到的是这个

if (whatever)
    if (xyzzy) asdf();
    else baz;

显然这是一个 bug。

简单的解决方案是到处要求 `{...}`,但我还有另一个更喜欢的方法,即使有编码标准要求到处都是 `{...}`(以防有人忘记):在宏定义中添加一个平衡的 `else`

#define MYMACRO(a,b) \                (Good)
    if (xyzzy) asdf(); \
    else (void)0

(`(void)0` 会导致编译器在你忘记在“调用”后加上 `;` 时生成错误消息。)

你对该宏的使用可能如下所示

if (whatever)
    MYMACRO(foo,bar);
                    ↑ // This ; closes off the else (void)0 part
else
    baz;

它将被扩展为一组平衡的 `if` 和 `else`

if (whatever)
    if (xyzzy)
        asdf();
    else
        (void)0;
        ↑↑↑↑↑↑↑↑ // A do-nothing statement
else
    baz;

就像我说的,即使编码标准要求所有 `if` 语句都使用 `{...}`,我个人也会这样做。你可以说我偏执,但我晚上睡得更香,我的代码错误也更少。

还有一种老派 C 程序员会记得的方法

#define MYMACRO(a,b) \                (Okay)
    do { \
      if (xyzzy) asdf(); \
    } while (false)

有些人更喜欢 `do {...} while (false)` 方法,不过如果你选择使用它,请注意它可能会导致你的编译器生成效率较低的代码。这两种方法都会导致编译器在你忘记在 `MYMACRO(foo,bar)` 之后加上 `;` 时给你一个错误消息。

包含多行的宏应该如何处理?

尽可能避免宏。但是,是的,有时你仍然需要使用它们,当你使用时,请阅读此内容以了解一些编写包含多个语句的宏的安全方法。

这是一个天真的解决方案

#define MYMACRO(a,b) \                (Bad)
    statement1; \
    statement2; \
    /*...*/ \
    statementN;

如果有人在需要单个语句的上下文中使用宏,这可能会导致问题。例如:

while (whatever)
    MYMACRO(foo, bar);

天真的解决方案是将语句包装在 `{...}` 中,例如这样

#define MYMACRO(a,b) \                (Bad)
    { \
        statement1; \
        statement2; \
        /*...*/ \
        statementN; \
    }

但这将导致像下面这样的编译时错误

if (whatever)
    MYMACRO(foo, bar);
else
    baz;

...因为编译器会看到

if (whatever)
{
    statement1;
    statement2;
    // ...
    statementN;
} ; else
↑↑↑↑↑↑↑↑ // Compile-time error!
    baz;

一个解决方案是使用 `do {` *<statements go here>* `} while (false)` 伪循环。这将精确地执行“循环”的主体一次。宏可能看起来像这样

#define MYMACRO(a, b) \                (Okay)
    do { \
        statement1; \
        statement2; \
        /*...*/ \
        statementN; \
    } while (false)
                  ↑ // Intentionally not adding a ; here!

分号由宏的用户添加,例如

if (whatever)
    MYMACRO(foo, bar);
                     ↑ // The user of MYMACRO() adds the ; here
else
    baz;

扩展后,编译器将看到这个

if (whatever)
    do {
        statement1;
        statement2;
        // ...
        statementN;
    } while (false);
                   ↑ // From user's code, not from MYMACRO() itself
else
    baz;

上述方法有一个不太可能但可能存在的缺点:历史上,一些 C++ 编译器拒绝内联展开任何包含循环的函数。如果你的 C++ 编译器有此限制,它将不会内联展开任何使用 `MYMACRO()` 的函数。这很可能不是问题,要么因为你不在任何内联函数中使用 `MYMACRO()`,要么因为你的编译器(受其所有其他约束)愿意内联展开包含循环的函数(前提是内联函数满足你的编译器的所有其他要求)。但是,如果你担心,请用你的编译器进行一些测试:检查生成的汇编代码和/或执行一些简单的计时测试。

如果你的编译器在内联展开包含循环的函数方面有问题,你可以将 `MYMACRO()` 的定义更改为 `if (true) {`…` } else (void)0`

#define MYMACRO(a, b) \
    if (true) { \
        statement1; \
        statement2; \
        /*...*/ \
        statementN; \
    } else
        (void)0
               ↑ // Intentionally not adding a ; here!

扩展后,编译器将看到一组平衡的 `if` 和 `else`)

if (whatever)
    if (true) {
        statement1;
        statement2;
        // ...
        statementN;
    } else
        (void)0;
        ↑↑↑↑↑↑↑ // A do-nothing statement
else
    baz;

宏定义中的 `(void)0` 强制用户记住在任何宏使用后加上 `;`。如果你像这样忘记了 `;`……

foo();
MYMACRO(a, b)
             ↑ // Whoops, forgot the ; here
bar();
baz();

……那么展开后编译器会看到这个

foo();
if (true) {
    statement1; \
    statement2; \
    /*...*/ \
    statementN; \
} else
    (void)0 bar();
            ↑↑↑↑↑ // Fortunately(!) this will produce a compile-time error-message
baz();

尽管具体的错误消息可能会令人困惑,但它至少会使程序员注意到有问题。这比另一种情况要好得多:在 `MYMACRO()` 定义中没有 `(void)0`,编译器会悄悄地生成错误的代码:`bar()` 调用将永远不会被调用,因为它将错误地位于 `if` 的不可达 `else` 分支上。

需要拼接两个 token 的宏应该如何处理?

哎呀。我真的很讨厌宏。是的,它们有时很有用,是的,我确实使用它们。但我总是事后洗手。两次。宏以四种不同的方式邪恶邪恶#1邪恶#2邪恶#3邪恶#4

我们又来了,拼命地试图让一个本质邪恶的东西变得不那么邪恶

首先,基本方法是使用 ISO/ANSI C 和 ISO/ANSI C++ 的“token 拼接”特性:`##`。表面上看起来像这样

假设你有一个名为“MYMACRO”的宏,并且你将一个标记作为该宏的参数传入,并且你希望将该标记与标记“Tmp”连接起来以创建变量名。例如,使用 `MYMACRO(Foo)` 将创建一个名为 `FooTmp` 的变量,而使用 `MYMACRO(Bar)` 将创建一个名为 `BarTmp` 的变量。在这种情况下,天真的方法是这样说

#define MYMACRO(a) \
    /*...*/ a ## Tmp /*...*/

但是,当你使用 `##` 时,你需要一个双层间接。基本上,你需要创建一个特殊的宏用于“token 拼接”,例如

#define NAME2(a,b)         NAME2_HIDDEN(a,b)
#define NAME2_HIDDEN(a,b)  a ## b

请相信我——你真的需要这样做!(而且不要有人写信告诉我,没有第二层间接有时也有效。试着将一个符号与 `__LINE__` 连接起来,看看会发生什么。)

然后将你的 `a ## Tmp` 用法替换为 `NAME2(a,Tmp)`

#define MYMACRO(a) \
    /*...*/ NAME2(a,Tmp) /*...*/

如果你需要进行三向连接(例如,将三个 token 拼接在一起),你可以像这样创建一个 `NAME3()` 宏

#define NAME3(a,b,c)         NAME3_HIDDEN(a,b,c)
#define NAME3_HIDDEN(a,b,c)  a ## b ## c

为什么编译器在 ` #include "c:\test.h"` 中找不到我的头文件?

因为 `"\t"` 是一个制表符。

在你的 `#include` 文件名中,你应该使用正斜杠(`/`)而不是反斜杠(`\`),即使在使用反斜杠的操作系统(如 DOS、Windows、OS/2 等)上也是如此。例如

#if 1
  #include "/version/next/alpha/beta/test.h"    // RIGHT!
#else
  #include "\version\next\alpha\beta\test.h"    // WRONG!
#endif

请注意,你的所有文件名都应该使用正斜杠(`/`),而不仅仅是 `#include` 文件。

请注意,你的特定编译器可能不会将头文件名称中的反斜杠与字符串字面量中的反斜杠视为相同。例如,你的特定编译器可能会将 `#include "foo\bar\baz"` 视为其中的 `\` 字符已被引用。这是因为头文件名称和字符串字面量是不同的:你的编译器将始终以通常的方式解析字符串字面量中的反斜杠,例如 `'\t'` 会变成制表符等,但它可能不会使用相同的规则解析头文件名称。无论如何,你仍然不应该在头文件名称中使用反斜杠,因为这样做只会失去,而没有获得。

C++ `for` 循环的范围规则是什么?

在 `for` 语句中声明的循环变量是局部于循环体的。

以下代码以前是合法的,但现在不再合法,因为 `i` 的作用域现在只在 `for` 循环内部

for (int i = 0; i < 10; ++i) {
  // ...
  if ( /* something weird */ )
    break;
  // ...
}

if (i != 10) {
  // We exited the loop early; handle this situation separately
  // ...
}

如果你正在处理一些在 `for` 循环后使用 `for` 循环变量的旧代码,编译器会(希望!)给你一个警告或错误消息,例如“变量 `i` 不在作用域内”。

不幸的是,在某些情况下,旧代码会顺利编译,但会做一些不同的事情——错误的事情。例如,如果旧代码有一个全局变量 `i`,则上述代码 `if (i != 10)` 会在旧规则下从 `for` 循环变量 `i` 静默改变为当前规则下的全局变量 `i`。这不好。如果你担心,你应该咨询你的编译器,看看它是否有某种选项可以强制它对你的旧代码使用旧规则。

注意:你应该避免在嵌套作用域中使用相同的变量名,例如全局 `i` 和局部 `i`。事实上,你应该尽可能避免使用全局变量。如果你的旧代码遵循了这些编码标准,你将不会受到很多事情的影响,包括 `for` 循环变量的作用域规则。

注意:如果你的新代码可能会用旧编译器编译,你可能希望在 `for` 循环周围加上 `{...}`,即使是旧编译器也能强制将循环变量的作用域限定在循环中。并且尽量避免使用宏来达到此目的。记住:宏以四种不同的方式邪恶邪恶#1邪恶#2邪恶#3邪恶#4

为什么我不能通过返回类型重载函数?

如果你同时声明 `char f()` 和 `float f()`,编译器会给你一个错误消息,因为简单地调用 `f()` 将会产生歧义。

什么是“持久性”?什么是“持久对象”?

持久对象可以在创建它的程序停止后继续存在。持久对象甚至可以比创建程序的各种版本、磁盘系统、操作系统,甚至创建时运行操作系统的硬件更长久地存在。

持久对象的挑战在于有效地将其成员函数代码与其数据位(以及所有成员对象、所有其成员对象和基类的数据位和成员函数代码等)一起存储到辅助存储中。当你必须自己完成时,这不是一件简单的事情。在 C++ 中,你必须自己完成。C++/OO 数据库可以帮助隐藏所有这些机制。

我如何创建两个相互了解的类?

使用前向声明。

有时你必须创建两个相互使用的类。这称为循环依赖。例如

class Fred {
public:
  Barney* foo();  // Error: Unknown symbol 'Barney'
};

class Barney {
public:
  Fred* bar();
};

`Fred` 类有一个返回 `Barney*` 的成员函数,而 `Barney` 类有一个返回 `Fred*` 的成员函数。你可以使用“前向声明”来告知编译器某个类或结构的存在

class Barney;

这行代码必须出现在 `Fred` 类的声明之前。它只是通知编译器名称 `Barney` 是一个类,并且进一步向编译器承诺你最终将提供该类的完整定义。

前向声明与成员对象一起使用时需要哪些特殊考虑?

类的声明顺序至关重要。

如果第一个类包含第二个类的对象(而不是指向对象的指针),编译器会给你一个编译时错误。例如,

class Fred;  // Okay: forward declaration

class Barney {
  Fred x;  // Error: The declaration of Fred is incomplete
};

class Fred {
  Barney* y;
};

解决此问题的一种方法是颠倒类的顺序,以便在使用它的类之前定义“被使用”的类

class Barney;  // Okay: forward declaration

class Fred {
  Barney* y;  // Okay: the first can point to an object of the second
};

class Barney {
  Fred x;  // Okay: the second can have an object of the first
};

请注意,每个类完全包含另一个类的对象是永远不合法的,因为那意味着无限大的对象。换句话说,如果 `Fred` 的一个实例包含一个 `Barney`(而不是一个 `Barney*`),而一个 `Barney` 包含一个 `Fred`(而不是一个 `Fred*`),编译器会给你一个错误。

前向声明与内联函数一起使用时需要哪些特殊考虑?

类的声明顺序至关重要。

如果第一个类包含一个调用第二个类成员函数的内联函数,编译器会给你一个编译时错误。例如:

class Fred;  // Okay: forward declaration

class Barney {
public:
  void method()
  {
    x->yabbaDabbaDo();  // Error: Fred used before it was defined
  }
private:
  Fred* x;  // Okay: the first can point to an object of the second
};

class Fred {
public:
  void yabbaDabbaDo();
private:
  Barney* y;
};

有多种方法可以解决此问题。一种解决方法是在 `Fred` 类定义下方(但仍在该头文件中)使用 `inline` 关键字定义 `Barney::method()`。另一种方法是在文件 `Barney.cpp` 中不带 `inline` 关键字定义 `Barney::method()`。第三种方法是使用嵌套类。第四种方法是颠倒类的顺序,以便在使用它的类之前定义“被使用”的类

class Barney;  // Okay: forward declaration

class Fred {
public:
  void yabbaDabbaDo();
private:
  Barney* y;  // Okay: the first can point to an object of the second
};

class Barney {
public:
  void method()
  {
    x->yabbaDabbaDo();  // Okay: Fred is fully defined at this point
  }
private:
  Fred* x;
};

请记住:每当你使用前向声明时,你只能使用该符号;你不能做任何需要了解前向声明类的操作。特别是,你不能访问第二个类的任何成员。

为什么我不能将前向声明的类放入 `std::vector<>` 中?

因为 `std::vector<>` 模板需要知道其包含元素的 `sizeof()`,而且 `std::vector<>` 可能会访问包含元素的成员(例如复制构造函数、析构函数等)。例如,

class Fred;  // Okay: forward declaration

class Barney {
  std::vector<Fred> x;  // Error: the declaration of Fred is incomplete
};

class Fred {
  Barney* y;
};

解决此问题的一种方法是将 `Barney` 更改为使用 `Fred` 指针的 `std::vector<>`(原始指针或智能指针,如 unique_ptr 或 shared_ptr),而不是 `Fred` 对象的 `std::vector<>`

class Fred;  // Okay: forward declaration

class Barney {
  std::vector<std::unique_ptr<Fred>> x;  // Okay: Barney can use Fred pointers
};

class Fred {
  Barney* y;
};

解决此问题的另一种方法是颠倒类的顺序,以便在 `Barney` 之前定义 `Fred`

class Barney;  // Okay: forward declaration

class Fred {
  Barney* y;  // Okay: the first can point to an object of the second
};

class Barney {
  std::vector<Fred> x;  // Okay: Fred is fully defined at this point
};

请记住:每当你将一个类用作模板参数时,该类的声明必须是完整的,而不仅仅是前向声明

为什么有些人认为 `x = ++y + y++` 不好?

因为它是未定义的行为,这意味着运行时系统被允许执行奇怪甚至离谱的事情。

C++ 语言规定,在两个序列点之间,变量不能被修改多次。引自标准(第 5 节,第 4 段)

在当前和下一个序列点之间,一个标量对象的存储值最多只能通过表达式的求值修改一次。此外,先前的值只能用于确定要存储的值。

`i++ + i++` 的值是多少?

它是未定义的。基本上,在 C 和 C++ 中,如果你在一个表达式中既读取又写入一个变量两次,结果就是未定义的。不要这样做。另一个例子是

    v[i] = i++;

相关示例

    f(v[i],i++);

这里,结果是未定义的,因为函数参数的求值顺序是未定义的。

评估顺序未定义被认为是能产生性能更好的代码。编译器可以对此类示例发出警告,这些示例通常是细微的错误(或潜在的细微错误)。令人失望的是,几十年后,大多数编译器仍然不发出警告,将这项工作留给了专门的、独立的、未充分利用的工具。

“序列点”是什么意思?

注意:C++11 标准以不同的方式表达了以下相同的规则。它不再提及“序列点”,但效果应与下述相同。

C++98 标准规定 (1.9p7)

在执行序列中某些指定的点,称为序列点,所有先前求值的副作用都应完成,且后续求值的副作用不应发生。

例如,如果一个表达式包含子表达式 `y++`,那么变量 `y` 将在下一个序列点处递增。此外,如果序列点之后的表达式包含子表达式 `++z`,那么 `z` 在达到序列点时尚未递增。

被称为序列点的“某些指定点”是(节和段落号来自标准

  • 分号 (1.9p16)
  • 非重载逗号运算符 (1.9p18)
  • 非重载 `||` 运算符 (1.9p18)
  • 非重载 `&&` 运算符 (1.9p18)
  • 三元 `?:` 运算符 (1.9p18)
  • 在评估完函数的所有参数之后,但在函数内第一个表达式执行之前 (1.9p17)
  • 函数的返回对象被复制回调用者之后,但在紧随调用之后的代码尚未求值之前 (1.9p17)
  • 在每个基类和成员初始化之后 (12.6.2p3)