1

我已经用 Java 和 C 编程过,现在我正试图用 C++ 来弄脏我的手。

鉴于此代码:

class Booth {

private :
          int tickets_sold;
public :
          int get_tickets_sold();
          void set_tickets_sold();
};

在 Java 中,只要我需要 的值tickets_sold,我就会反复调用 getter。

例如:

if (obj.get_tickets_sold() > 50 && obj.get_tickets_sold() < 75){
     //do something
}

在 CI 中只会获取结构中特定变量的值:

if( obj_t->tickets_sold > 50 && obj_t->tickets_sold < 75){
    //do something
}

因此,当在 C 中使用结构时,我节省了在 Java 中进行的两个调用,即两个 getter,我什至不确定这些是实际调用还是 Java 以某种方式内联这些调用。

我的观点是,如果我在 C++ 中也使用在 Java 中使用的相同技术,这两个对 getter 成员函数的调用是否会花费我,或者编译器会以某种方式知道内联代码?(从而完全减少函数调用的开销?)

或者,我最好使用:

int num_tickets = 0;

if ( (num_tickets = obj.get_ticket_sold()) > 50 && num_tickets < 75){
    //do something
}

我想编写紧凑的代码并避免不必要的函数调用,我会在 Java 中关心这一点,因为我们都知道原因。但是,我希望我的代码可读,并使用privateandpublic关键字来正确反映要做什么。

4

7 回答 7

8

除非您的程序太慢,否则这并不重要。在 99.9999% 的代码中,函数调用的开销是微不足道的。编写最清晰、最容易维护、最容易理解的代码,并且只有在知道性能热点在哪里(如果有的话)之后才开始调整性能。

也就是说,现代 C++ 编译器(和一些链接器)可以并且将会内联函数,尤其是像这样的简单函数。

于 2011-02-24T04:38:29.750 回答
5

如果你只是学习语言,你真的不应该担心这个。考虑它足够快,直到证明不是这样。也就是说,这里有很多误导性或不完整的答案,所以为了记录,我将充实一些更微妙的含义。考虑你的班级:

class Booth
{
  public:
    int get_tickets_sold();
    void set_tickets_sold();
  private:
    int tickets_sold;
};

get 和 set 函数的实现(称为定义)尚未指定。如果你在类声明中指定了函数体,那么编译器会认为你已经隐式地要求它们被内联(但如果它们太大可能会忽略它)。如果您稍后使用inline关键字指定它们,那将具有完全安全的效果。总结...

class Booth
{
  public:
    int get_tickets_sold() { return tickets_sold; }
    ...

...和...

class Booth
{
  public:
    int get_tickets_sold();
    ...
};

inline int Booth::get_tickets_sold() { return tickets_sold; }

...是等价的(至少在标准鼓励我们期望的方面,但个别编译器启发式可能会有所不同 - 内联是编译器可以自由忽略的请求)。

如果函数体稍后在没有inline关键字的情况下指定,则编译器没有义务内联它们,但仍然可以选择这样做。如果它们出现在同一个翻译单元中(即在您正在编译的 .cc/.cpp/.c++/etc.“实现”文件中或它直接或间接包含的某些头文件中),则更有可能这样做。 如果实现只在链接时可用,那么函数可能根本没有内联,但这取决于您的特定编译器和链接器交互和协作的方式。这不仅仅是启用优化和期待魔法的问题。为了证明这一点,请考虑以下代码:

// inline.h:
void f();

// inline.cc:
#include <cstdio>
void f() { printf("f()\n"); }

// inline_app.cc:
#include "inline.h"
int main() { f(); }

构建这个:

g++ -O4 -c inline.cc
g++ -O4 -o inline_app inline_app.cc inline.o

调查内联:

$ gdb inline_app 
...
(gdb) break main
Breakpoint 1 at 0x80483f3
(gdb) break f
Breakpoint 2 at 0x8048416
(gdb) run
Starting program: /home/delroton/dev/inline_app 

Breakpoint 1, 0x080483f3 in main ()
(gdb) next
Single stepping until exit from function main, 
which has no line number information.

Breakpoint 2, 0x08048416 in f ()
(gdb) step
Single stepping until exit from function _Z1fv, 
which has no line number information.
f()
0x080483fb in main ()
(gdb) 

请注意,执行从 main() 中的 0x080483f3 到 f() 中的 0x08048416,然后回到 main() 中的 0x080483fb ......显然没有内联。这说明不能仅仅因为函数的实现微不足道而期望内联。

请注意,此示例使用目标文件的静态链接。显然,如果您使用库文件,您实际上可能希望避免专门对函数进行内联,以便您可以更新库而无需重新编译客户端代码。对于无论如何在加载时隐式完成链接的共享库,它甚至更有用。

很多时候,提供普通函数的类使用两种形式的预期内联函数定义(即在类内或 withinline关键字),如果这些函数可以预期在任何性能关键循环内被调用,但相反的考虑是通过内联一个您强制客户端代码重新编译(相对较慢,可能没有自动触发器)和重新链接(快速,因为共享库在下次执行时发生),而不是仅仅重新链接,以便获取对函数实现的更改。

这些考虑很烦人,但是对这些权衡的刻意管理是允许企业使用 C 和 C++ 扩展到数以亿计的行和数千个单独的项目,几十年来所有这些项目都共享各种库。

另一个小细节:作为一个大概的数字,外联 get/set 函数通常比等效的内联代码慢一个数量级 (10x)。这显然会因 CPU、编译器、优化级别、变量类型、缓存命中/未命中等而有所不同。

于 2011-02-24T04:51:21.277 回答
4

不,重复调用成员函数不会受到伤害。

如果它只是一个 getter 函数,它几乎肯定会被 C++ 编译器内联(至少在发布/优化的构建中),Java 虚拟机可能会“发现”某个函数被频繁调用并为此进行优化。所以一般来说使用函数几乎没有性能损失。

您应该始终首先编写可读性代码。当然,这并不是说您应该完全忽略性能,但是如果性能不可接受,那么您始终可以分析您的代码并查看最慢的部分在哪里。

此外,通过限制对tickets_soldgetter 函数后面的变量的访问,您几乎可以保证唯一可以将tickets_sold变量修改为Booth. 这允许您在程序行为中强制执行不变量。

例如,tickets_sold显然不会是负值。那是结构的不变量。您可以通过将其设为tickets_sold私有并确保您的成员函数不违反该不变量来强制执行该不变量。该类通过 getter 函数作为“只读数据成员”提供给其他所有人,并且仍然保留不变量Boothtickets_sold

将其设为公共变量意味着任何人都可以去践踏其中的数据tickets_sold,这基本上完全破坏了您在其上强制执行任何不变量的能力tickets_sold。这使得有人可以将负数写入tickets_sold,这当然是荒谬的。

于 2011-02-24T04:38:20.573 回答
1

编译器很可能会像这样内联函数调用。

于 2011-02-24T04:38:25.373 回答
1
class Booth {
public:
    int get_tickets_sold() const { return tickets_sold; }

private:
    int tickets_sold;
};

您的编译器应该inline get_tickets_sold,如果没有,我会感到非常惊讶。如果没有,您需要使用新的编译器或打开优化。

于 2011-02-24T04:38:30.570 回答
0

任何称职的编译器都可以轻松地将 getter 优化为直接成员访问。唯一不会发生的情况是当您明确禁用优化(例如,用于调试构建)或者如果您使用的是脑死编译器(在这种情况下,您应该认真考虑放弃它而使用真正的编译器)。

于 2011-02-24T04:46:12.713 回答
0

编译器很可能会为您完成这项工作,但总的来说,对于这样的事情,我会更多地从 C 角度而不是 Java 角度来处理它,除非您想让成员访问 const 引用。但是,在处理整数时,在副本上使用 const 引用通常没有什么价值(至少在 32 位环境中,因为两者都是 4 个字节),所以你的例子在这里并不是一个很好的例子......也许这可能说明为什么在 C++ 中使用 getter/setter:

class StringHolder
{
public:
  const std::string& get_string() { return my_string; }
  void set_string(const std::string& val) { if(!val.empty()) { my_string = val; } }
private
  std::string my_string;
}

这可以防止修改,除非通过设置器,然后允许您执行额外的逻辑。然而,在这样一个简单的类中,这个模型的值是 nil,你只是让调用它的编码器输入更多,并没有真正增加任何值。对于这样的课程,我不会有 getter/setter 模型。

于 2011-02-24T04:46:38.827 回答