C++在循环内和循环外定义变量的差异(如何写出高效的for循环)

写这篇文章的原因是我在问答平台看到的一个问题:

C++内层循环中定义变量和在外面定义比影响大吗?

问题来自:http://ask.csdn.net/questions/176270

例如:

for(int i=0;i<999;i++) {

for(int j=0;j<999;j++);

}

内层循环每次都定义j会造成多大的消耗呢?

此处我给出的回答是:

这个需要看你具体用什么编译器。不过主流编译器(如vs和gcc)这一块优化都比较好,不会反复分配变量。

看到答案和评论,好像有很多人对这个感兴趣,所以我打算给大家实测分享一下,于是写了如下代码进行测试:

#include <cstdio>
using namespace std;

void Test1()
{
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }

}

void Test2()
{
    int i, j;

    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }
}

int main()
{
    Test1();
    Test2();

    return 0;
}

OK,程序非常简单,Test1Test2是两个循环,干相同的事情,就是在双重循环里打印一下 ij 的值,差别只在于一个在循环外定义变量 j,另一个在循环内定义变量 j

此处我使用g++进行编译,优化等级是O0(这是GCC默认的优化等级,也是最低的优化等级)的:

g++ -O0 -g test.cpp

编译后,我将生成的Test1函数和Test2函数反汇编出来,得出的结果是这样的:

Test1函数反汇编如下:

(gdb) disas /m Test1
Dump of assembler code for function Test1():
5       {
   0x0804841d <+0>:     push   %ebp
   0x0804841e <+1>:     mov    %esp,%ebp
   0x08048420 <+3>:     sub    $0x28,%esp

6           for (int i = 0; i < 2; i++)
   0x08048423 <+6>:     movl   $0x0,-0x10(%ebp)
   0x0804842a <+13>:    jmp    0x804845d <Test1()+64>
   0x08048459 <+60>:    addl   $0x1,-0x10(%ebp)
   0x0804845d <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x08048461 <+68>:    jle    0x804842c <Test1()+15>

7           {
8               for (int j = 0; j < 3; j++)
   0x0804842c <+15>:    movl   $0x0,-0xc(%ebp)
   0x08048433 <+22>:    jmp    0x8048453 <Test1()+54>
   0x0804844f <+50>:    addl   $0x1,-0xc(%ebp)
   0x08048453 <+54>:    cmpl   $0x2,-0xc(%ebp)
   0x08048457 <+58>:    jle    0x8048435 <Test1()+24>

9               {
10                  printf("%d,%d\n", int(i), int(j));
   0x08048435 <+24>:    mov    -0xc(%ebp),%eax
   0x08048438 <+27>:    mov    %eax,0x8(%esp)
   0x0804843c <+31>:    mov    -0x10(%ebp),%eax
   0x0804843f <+34>:    mov    %eax,0x4(%esp)
   0x08048443 <+38>:    movl   $0x8048560,(%esp)
   0x0804844a <+45>:    call   0x80482f0 <printf@plt>

11              }
12          }
13
14      }
   0x08048463 <+70>:    leave
   0x08048464 <+71>:    ret

Test2函数反汇编如下:

(gdb) disas /m Test2
Dump of assembler code for function Test2():
17      {
   0x08048465 <+0>:     push   %ebp
   0x08048466 <+1>:     mov    %esp,%ebp
   0x08048468 <+3>:     sub    $0x28,%esp

18          int i, j;
19
20          for (i = 0; i < 2; i++)
   0x0804846b <+6>:     movl   $0x0,-0x10(%ebp)
   0x08048472 <+13>:    jmp    0x80484a5 <Test2()+64>
   0x080484a1 <+60>:    addl   $0x1,-0x10(%ebp)
   0x080484a5 <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x080484a9 <+68>:    jle    0x8048474 <Test2()+15>

21          {
22              for (j = 0; j < 3; j++)
   0x08048474 <+15>:    movl   $0x0,-0xc(%ebp)
   0x0804847b <+22>:    jmp    0x804849b <Test2()+54>
   0x08048497 <+50>:    addl   $0x1,-0xc(%ebp)
   0x0804849b <+54>:    cmpl   $0x2,-0xc(%ebp)
   0x0804849f <+58>:    jle    0x804847d <Test2()+24>

23              {
24                  printf("%d,%d\n", int(i), int(j));
   0x0804847d <+24>:    mov    -0xc(%ebp),%eax
   0x08048480 <+27>:    mov    %eax,0x8(%esp)
   0x08048484 <+31>:    mov    -0x10(%ebp),%eax
   0x08048487 <+34>:    mov    %eax,0x4(%esp)
   0x0804848b <+38>:    movl   $0x8048560,(%esp)
   0x08048492 <+45>:    call   0x80482f0 <printf@plt>

25              }
26          }
27      }
   0x080484ab <+70>:    leave
   0x080484ac <+71>:    ret    

End of assembler dump.

Test1的反汇编中,我们在内部for (int j = 0; j < 3; j++)下面,没有看到分配变量 j 的汇编指令,如果再只打印Test1Test2的汇编代码,经过对比,你们发现这两个函数产生的汇编指令是完全一样的:

(gdb) disas Test1
Dump of assembler code for function Test1():
   0x0804841d <+0>:     push   %ebp
   0x0804841e <+1>:     mov    %esp,%ebp
   0x08048420 <+3>:     sub    $0x28,%esp
   0x08048423 <+6>:     movl   $0x0,-0x10(%ebp)
   0x0804842a <+13>:    jmp    0x804845d <Test1()+64>
   0x0804842c <+15>:    movl   $0x0,-0xc(%ebp)
   0x08048433 <+22>:    jmp    0x8048453 <Test1()+54>
   0x08048435 <+24>:    mov    -0xc(%ebp),%eax
   0x08048438 <+27>:    mov    %eax,0x8(%esp)
   0x0804843c <+31>:    mov    -0x10(%ebp),%eax
   0x0804843f <+34>:    mov    %eax,0x4(%esp)
   0x08048443 <+38>:    movl   $0x8048560,(%esp)
   0x0804844a <+45>:    call   0x80482f0 <printf@plt>
   0x0804844f <+50>:    addl   $0x1,-0xc(%ebp)
   0x08048453 <+54>:    cmpl   $0x2,-0xc(%ebp)
   0x08048457 <+58>:    jle    0x8048435 <Test1()+24>
   0x08048459 <+60>:    addl   $0x1,-0x10(%ebp)
   0x0804845d <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x08048461 <+68>:    jle    0x804842c <Test1()+15>
   0x08048463 <+70>:    leave
   0x08048464 <+71>:    ret
End of assembler dump.
(gdb) disas Test2
Dump of assembler code for function Test2():
   0x08048465 <+0>:     push   %ebp
   0x08048466 <+1>:     mov    %esp,%ebp
   0x08048468 <+3>:     sub    $0x28,%esp
   0x0804846b <+6>:     movl   $0x0,-0x10(%ebp)
   0x08048472 <+13>:    jmp    0x80484a5 <Test2()+64>
   0x08048474 <+15>:    movl   $0x0,-0xc(%ebp)
   0x0804847b <+22>:    jmp    0x804849b <Test2()+54>
   0x0804847d <+24>:    mov    -0xc(%ebp),%eax
   0x08048480 <+27>:    mov    %eax,0x8(%esp)
   0x08048484 <+31>:    mov    -0x10(%ebp),%eax
   0x08048487 <+34>:    mov    %eax,0x4(%esp)
   0x0804848b <+38>:    movl   $0x8048560,(%esp)
   0x08048492 <+45>:    call   0x80482f0 <printf@plt>
   0x08048497 <+50>:    addl   $0x1,-0xc(%ebp)
   0x0804849b <+54>:    cmpl   $0x2,-0xc(%ebp)
   0x0804849f <+58>:    jle    0x804847d <Test2()+24>
   0x080484a1 <+60>:    addl   $0x1,-0x10(%ebp)
   0x080484a5 <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x080484a9 <+68>:    jle    0x8048474 <Test2()+15>
   0x080484ab <+70>:    leave
   0x080484ac <+71>:    ret
End of assembler dump.

当然,这里只测试了g++的编译效果。vs下的效果大家可以自己测试。目前可以肯定,如果你使用gcc的编译器,你完全可以不用纠结在循环外定义变量还是循环内定义变量,因为效果完全是一样的,不过为了代码好看,还是写到循环内吧。

上面已经探究了使用基本数据类型int作为循环变量的情况,这里需要进阶一下,探讨一下如果我使用的不是int,而是一个复杂的对象,那循环的效果又是如何呢?

为了方便看到变量的分配,我在类的构造函数里加了打印语句,可以让我们方便地看到类的对象被创建的情况:

#include <cstdio>
using namespace std;

class MyInt
{
public:
    MyInt(int i):
        m_iValue(i)
    {
        printf("Constructed: MyInt(%d)\n", i);
    }

    MyInt()
    {
        printf("Constructed: MyInt()\n");
    }

    MyInt &operator++(int i)
    {
        m_iValue ++;
        return *this;
    }

    bool const operator <(const MyInt& another)
    {
        return m_iValue < another.m_iValue;
    }

    operator int()
    {
        return m_iValue;
    }

    MyInt &operator =(int i)
    {
        m_iValue = i;
        return *this;
    }

private:
    int m_iValue;
};

void Test1()
{
    for (MyInt i = MyInt(0); i < MyInt(2); i++)
    {
        for (MyInt j = MyInt(0); j < MyInt(3); j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }
}
void Test2()
{
    MyInt i, j;

    for (i = MyInt(0); i < MyInt(2); i++)
    {
        for (j = MyInt(0); j < MyInt(3); j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }
}

void Test3()
{
    MyInt i, j;

    for (i = 0; int(i) < 2; i++)
    {
        for (j = 0; int(j) < 3; j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }
}

int main()
{
    printf("Test1---------------------------------\n");
    Test1();

    printf("Test2---------------------------------\n");
    Test2();

    printf("Test3---------------------------------\n");
    Test3();

    return 0;
}

好的,还是使用g++ -O0编译,我们来看看执行结果:

Test1---------------------------------
Constructed: MyInt(0)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
0,0
Constructed: MyInt(3)
0,1
Constructed: MyInt(3)
0,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
1,0
Constructed: MyInt(3)
1,1
Constructed: MyInt(3)
1,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Test2---------------------------------
Constructed: MyInt()
Constructed: MyInt()
Constructed: MyInt(0)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
0,0
Constructed: MyInt(3)
0,1
Constructed: MyInt(3)
0,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
1,0
Constructed: MyInt(3)
1,1
Constructed: MyInt(3)
1,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Test3---------------------------------
Constructed: MyInt()
Constructed: MyInt()
0,0
0,1
0,2
1,0
1,1
1,2

可以看到,Test3创建对象的次数是最少的,如果对象比较复杂,显然Test3会是最高效的编码方式。

对于整个程序的输出,我们可以分析一下:

  • 对于C++内置的基本数据类型,编译器有相关的优化,在双重循环中会避免掉对象的反复分配,但对于复杂的类对象,编译器似乎不会轻易优化,所以我们在Test1中仍然看到了对j变量多次分配动作。
  • Test2中,由于我们在循环外定义了j变量,所以这里没有发生对j变量的反复分配,但由于赋值条件i = MyInt(0)j = MyInt(0)以及判断条件i < MyInt(2)j < MyInt(3)中需要构造MyInt(2)MyInt(3)对象,所以我们仍然看到循环中多次的变量分配。
  • 而在Test3中,我们换了一种方式,用重载运算符=直接在赋值语句中给对象赋整型值,避免了赋值语句中创建MyInt对象,并用int(i) < 2int(j) < 3,避免了在判断条件里创建MyInt对象,所以整段代码里只在循环外分配了两次变量,这其实是最高效的方式。

最后总结:

  1. 对于使用int等基本数据类型作为循环变量,只要你用的优化方面足够给力的主流的编译器,完全不需要关心在循环外还是循环内定义循环变量。
  2. 如果循环变量本身是复杂的对象,建议在循环外定义好,并且在for循环的赋值语句、判断语句中,都要避免重复创建对象。
时间: 2024-10-25 18:55:40

C++在循环内和循环外定义变量的差异(如何写出高效的for循环)的相关文章

js和PHP等脚本语言for循环和if语句里面定义变量的作用域

在js和PHP等脚本语言中for循环中的定义变量,会先把var l 提到for循环外面,(只会定义一次l),所以for循环外面可以输出l var a=0; if(a==0) { var i=1; } alert(i); for(var j=0;j 在c#等语言中 for(int i=0;ijs和PHP等脚本语言for循环和if语句里面定义变量的作用域

c#变量在for循环内声明与外的区别

1.这样写是错误的 #region 有这样的一个字符串类型的数组{"老杨","老苏","老马","老牛"};要求 变换成这样一个字符串:老杨|老苏|老马|老牛 string[] str = { "老杨", "老苏", "老马", "老牛" }; for (int i = 0; i < str.Length - 1; i++) { string

while循环同样适用@python for循环内输出和外输出

# 原文: 通过for循环求和,结果发现输出完全不一样,一个循环是输出每一步的结果,另一个循环是输出最终一次的结果,今天终于弄懂了.如下所示: 原文链接:https://blog.csdn.net/elanewow/article/details/81286617 原文地址:https://www.cnblogs.com/qnmtt/p/9568970.html

ios给NSMutableDictionary循环赋值的两种方式,在循环内初始化NSMutableDictionary和在循环外初始化NSMutableDictionary有何区别?(已解决)

NSMutableArray * arrayName = [NSMutableArray array]; NSMutableArray * array = [NSMutableArray array]; [array removeAllObjects]; for (int i = 0; i< 10; i++) { NSString * str = [NSString stringWithFormat:@"name%i",i]; [arrayName addObject:str];

try-catch遇到循环时,将try代码块放在循环内还是循环外的选择

当循环内的代码出现异常,需要结束循环时,将try代码块放在循环外; 当循环内的代码出现异常,需要继续执行循环时,将try代码块放在循环内. public static void main(String[] args) { int runs = 3; //循环运行次数 //try代码块在循环外 try { for (int i = 0; i < runs; i++) { if (i == 0) { throw new RuntimeException("try在循环外时,出现运行异常&quo

for循环内变量定义问题

在C99之前的编译器,不支持括号内进行定义变量 2.在C++中是允许定义变量的,其c99之后的编译器亦可以在括号内进行声明变量. #include<stdio.h>//c++可以编译,但是c不能编译 int main() { int sum=0; for(int i=0;i<=100;++i)//定义并初始化变量i=0.实际编译器运行不起来.但把int i=0拿到for循环外部(如下黑体加粗部分)就可以. { sum+=i; } printf("%d",sum); r

C# 使用IEnumerable,yield 返回结果,同时使用foreach时,在循环内修改变量的值无效

在项目中遇到了一个比较奇怪的问题,在foreach循环中修改列表的值后没有生效,后面使用时还是获取列表时的值,原因是因为使用了 yield return .下面让我们来探究下其中的原因: 首先来看下 yield return 官方的解释 使用 yield return 语句可一次返回一个元素.通过 foreach 语句或 LINQ 查询来使用迭代器方法. foreach 循环的每次迭代都会调用迭代器方法. 迭代器方法运行到 yield return 语句时,会返回一个 expression,并保

c++学习笔记之基础---类内声明函数后在类外定义的一种方法

在C++的“类”中经常遇到这样的函数, 返回值类型名 类名::函数成员名(参数表){ 函数体.} 双冒号的作用 ::域名解析符!返回值类型名 类名::函数成员名(参数表) { 函数体. } 这个是在类内声明函数后在类外定义的一种方法!如果不加"类名::"的话,编译系统就不会知道你的函数属于哪个类;另外,这样定义函数一定要在类中声明之后,说明它是类的成员函数才可以!在类内声明的时候就不需要::了,直接 返回值类型 函数名(参数表) 就可以了!

C++类的成员函数(在类外定义成员函数、inline成员函数)

类的成员函数(简称类函数)是函数的一种,它的用法和作用和前面介绍过的函数基本上是一样的,它也有返回值和函数类型,它与一般函数的区别只是:它是属于一个类的成员,出现在类体中.它可以被指定为private(私有的).public (公用的)或protected(受保护的). 在使用类函数时,要注意调用它的权限(它能否被调用)以及它的作用域(函数能使用什么范围中的数据和函数).例如私有的成员函数只能被本类中的其它成员函数所调用,而不能被类外调用.成员函数可以访问本类中任何成员(包括私有的和公用的),可