【C++】提高程序运行效率的10个简单方法

对于每一个程序员来说,程序的运行效率都是一个值得重视,并为之付出努力的问题。但是程序性能的优化也是一门复杂的学问,需要很多的知识,然而并不是每个程序员都具备这样的知识,而且论述如何优化程序提高程序运行效率的书籍也很少。但是这并不等于我们可以忽略程序的运行效率,下面就介绍一下本人积累的一些简单实用的提高程序运行效率的方法,希望对大家有所帮助。

1. 尽量减少值传递,多用引用来传递参数

如果参数是int等语言自定义的类型可能能性能的影响还不是很大,但是如果参数是一个类的对象,那么其效率问题就不言而喻了。例如一个判断两个字符串是否相等的函数,其声明如下:

bool Compare(string s1, string s2)
bool Compare(string *s1, string *s2)
bool Compare(string &s1, string &s2)
bool Compare(const string &s1, const string &s2)

其中若使用第一个函数(值传递),则在参数传递和函数返回时,需要调用string的构造函数和析构函数两次(即共多调用了四个函数),而其他的三个函数(指针传递和引用传递)则不需要调用这四个函数。因为指针和引用都不会创建新的对象。如果一个构造一个对象和析构一个对象的开销是庞大的,这就是会效率造成一定的影响。

然而在很多人的眼中,指针是一个恶梦,使用指针就意味着错误,那么就使用引用吧!它与使用普通值传递一样方便直观,同时具有指针传递的高效和能力。因为引用是一个变量的别名,对其操作等同于对实际对象操作,所以当你确定在你的函数是不会或不需要变量参数的值时,就大胆地在声明的前面加上一个const吧,就如最后的一个函数声明一样。

同时加上一个const还有一个好处,就是可以对常量进行引用,若不加上const修饰符,引用是不能引用常量的。

2. ++i和i++引申出的效率问题

整型变量的前加和后加的区别相信大家是很清楚的。然而在这里我想跟大家谈的却是C++类的运算符重载,为了与整形变量的用法一致,在C++中重载运算符++时一般都会把前加和后加都重载。你可能会说,你在代码中不会重载++运算符,但是你敢说你没有使用过类的++运算符重载吗?迭代器类你总使用过吧!可能到现在你还不是很懂我在说什么,那么就先看看下面的例子吧,是本人为链表写的一个内部迭代器。

_SingleList::Iterator& _SingleList::Iterator::operator++()//前加
{
  pNote = pNote->pNext;
  return *this;
}
_SingleList::Iterator _SingleList::Iterator::operator++(int)//后加
{
  Iterator tmp(*this);
  pNote = pNote->pNext;
  return tmp;
}

从后加的实现方式可以知道,对象利用自己创建一个临时对象(自己在函数调用的一个复制),然后改变自己的状态,并返回这个临时对象;而前加的实现方式,直接改变自己的内部状态,并返回自己的引用。

由此可知:后加实现时会调用复制构造函数,在函数返回时还要调用析构函数,而由于前加实现方式直接改变对象的内部状态,并返回自己的引用,至始至终也没有创建新的对象,所以也就不会调用构造函数和析构函数。

然而更加糟糕的是,迭代器通常是用来遍历容器的,它大多应用在循环中,试想你的链表有100个元素,用下面的两种方式遍历:

for(_SingleList::Iterator it = list.begin(); it != list.end(); ++it)
{
  //do something
} 

for(_SingleList::Iterator it = list.begin(); it != list.end(); it++)
{
  //do something
} 

如果你的习惯不好,写了第二种形式,那么很不幸,做同样的事情,就是因为一个前加和一个后加的区别,你就要调用多200个函数,其对效率的影响可就不可忽视了。

3. 循环引发的讨论1(循环内定义,还是循环外定义对象)

请看以下两段代码:
代码1:

ClassTest CT;
for(int i = 0; i < 100; ++i)
{
  CT = a;
  //do something
}

代码2:

for(int i = 0; i < 100; ++i)
{
  ClassTest CT = a;
  //do something
}

你觉得哪段代码的运行效率较高呢?其实这种情况下,哪段代码的效率更高是不确定的,或者说是由这个类ClassTest本身决定的,分析如下:

对于代码1:需要调用ClassTest的构造函数1次,赋值操作函数(operator=)100次;对于代码2:需要高用(复制)构造函数100次,析构函数100次。

如果调用赋值操作函数的开销比调用构造函数和析构函数的总开销小,则第一种效率高,否则第二种的效率高。

4. 循环引发的讨论2(避免过大的循环)

请看以下两段代码:
代码1:

for(int i = 0; i < n; ++i)
{
  fun1();
  fun2();
}

代码2:

for(int i = 0; i < n; ++i)
{
  fun1();
}
for(int i = 0; i < n; ++i)
{
  fun2();
}

注:这里的fun1()和fun2()是没有关联的,即两段代码所产生的结果是一样的。

以代码的层面上来看,似乎是代码1的效率更高,因为毕竟代码1少了n次的自加运算和判断,毕竟自加运算和判断也是需要时间的。但是现实真的是这样吗?

这就要看fun1和fun2这两个函数的规模(或复杂性)了。如果这多个函数的代码语句很少,则代码1的运行效率高一些,但是若fun1和fun2的语句有很多,规模较大,则代码2的运行效率会比代码1显著高得多。可能你不明白这是为什么,要说是为什么这要由计算机的硬件说起。

由于CPU只能从内存中读取数据,而CPU的运算速度远远大于内存,所以为了提高程序的运行速度有效地利用CPU的能力,在内存与CPU之间有一个叫Cache的存储器,它的速度接近CPU。而Cache中的数据是从内存中加载而来的,这个过程需要访问内存,速度较慢。

这里先说说Cache的设计原理,就是时间局部性和空间局部性。时间局部性是指如果一个存储单元被访问,则可能该单元会很快被再次访问,这是因为程序存在着循环。空间局部性是指如果一个储存单元被访问,则该单元邻近的单元也可能很快被访问,这是因为程序中大部分指令是顺序存储、顺序执行的,数据一般也是以向量、数组、树、表等形式簇聚在一起的。

看到这里你可能已经明白其中的原因了。没错,就是这样!如果fun1和fun2的代码量很大,例如都大于Cache的容量,则在代码1中,就不能充分利用Cache了(由时间局部性和空间局部性可知),因为每循环一次,都要把Cache中的内容踢出,重新从内存中加载另一个函数的代码指令和数据,而代码2则更很好地利用了Cache,利用两个循环语句,每个循环所用到的数据几乎都已加载到Cache中,每次循环都可从Cache中读写数据,访问内存较少,速度较快,理论上来说只需要完全踢出fun1的数据1次即可。

5. 局部变量VS静态变量

很多人认为局部变量在使用到时才会在内存中分配储存单元,而静态变量在程序的一开始便存在于内存中,所以使用静态变量的效率应该比局部变量高,其实这是一个误区,使用局部变量的效率比使用静态变量要高。

这是因为局部变量是存在于堆栈中的,对其空间的分配仅仅是修改一次esp寄存器的内容即可(即使定义一组局部变量也是修改一次)。而局部变量存在于堆栈中最大的好处是,函数能重复使用内存,当一个函数调用完毕时,退出程序堆栈,内存空间被回收,当新的函数被调用时,局部变量又可以重新使用相同的地址。当一块数据被反复读写,其数据会留在CPU的一级缓存(Cache)中,访问速度非常快。而静态变量却不存在于堆栈中。可以说静态变量是低效的。

6. 避免使用多重继承

在C++中,支持多继承,即一个子类可以有多个父类。书上都会跟我们说,多重继承的复杂性和使用的困难,并告诫我们不要轻易使用多重继承。其实多重继承并不仅仅使程序和代码变得更加复杂,还会影响程序的运行效率。

这是因为在C++中每个对象都有一个this指针指向对象本身,而C++中的类对成员变量的使用是通过this的地址加偏移量来计算的,而在多重继承的情况下,这个计算会变量更加复杂,从而降低程序的运行效率。而为了解决二义性,而使用虚基类的多重继承对效率的影响更为严重,因为其继承关系更加复杂和成员变量所属的父类关系更加复杂。

7. 尽量少使用dynamic_cast

dynamic_cast的作用是进行指针或引用的类型转换,dynamic_cast的转换需要目标类型和源对象有一定的关系:继承关系。 实现从子类到基类的指针转换,实际上这种转换是非常低效的,对程序的性能影响也比较大,不可大量使用,而且继承关系越复杂,层次越深,其转换时间开销越大。在程序中应该尽量减少使用。

8. 减少除法运算的使用

无论是整数还是浮点数运算,除法都是一件运算速度很慢的指令,在计算机中实现除法是比较复杂的。所以要减少除法运算的次数,下面介绍一些简单方法来提高效率:

  1. 通过数学的方法,把除法变为乘法运算,如if(a > b/c),如果a、b、c都是正数,则可写成if(a*c > b)。
  2. 让编译器有优化的余地,如里你要做的运算是int型的n/8的话,写成(unsigned)n/8有利于编译器的优化。而要让编译器有优化的余地,则除数必须为常数,而这也可以用const修饰一个变量来达到目的。

9. 将小粒度函数声明为内联函数(inline)

正如我们所知,调用函数是需要保护现场,为局部变量分配内存,函数结束后还要恢复现场等开销,而内联函数则是把它的代码直接写到调用函数处,所以不需要这些开销,但会使程序的源代码长度变大。

所以若是小粒度的函数,如下面的Max函数,由于不需要调用普通函数的开销,所以可以提高程序的效率。

int Max(int a, int b)
{
  return a>b?a:b;
}

10. 多用直接初始化

与直接初始化对应的是复制初始化,什么是直接初始化?什么又是复制初始化?举个简单的例子,

ClassTest ct1;
ClassTest ct2(ct1);  //直接初始化
ClassTest ct3 = ct1;  //复制初始化

ClassTest ct1("ab");//直接初始化 
ClassTest ct2 = "ab";//复制初始化 
ClassTest ct3 = ct1;//复制初始化 
ClassTest ct4(ct1);//直接初始化 
ClassTest ct5 = ClassTest();//复制初始化 

那么直接初始化与复制初始化又有什么不同呢?直接初始化是直接以一个对象来构造另一个对象,如用ct1来构造ct2;复制初始化是先构造一个对象,再把另一个对象值复制给这个对象,如先构造一个对象ct3,再把ct1中的成员变量的值复制给ct3,从这里,可以看出直接初始化的效率更高一点。而且使用直接初始化还是一个好处,就是对于不能进行复制操作的对象,如流对象,是不能使用复制初始化的,只能进行直接初始化。可能我说得不太清楚,那么下面就引用一下经典吧!

以下是Primer是的原话:
“当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象”,还有一段这样说,“通常直接初始化和复制初始化仅在低级别优化上存在差异,然而,对于不支持复制的类型,或者使用非explicit构造函数的时候,它们有本质区别:
ifstream file1(“filename”): //ok:direct initialization
ifstream file2 = “filename”; //error:copy constructor is private

补充:
这里只是一点点的建议,虽然说了这么多,但是还是要说一下的就是:要避免不必要的优化,避免不成熟的优化,不成熟的优化是错误的来源,因为编译器会为你做很多你所不知道的优化。

  • 17
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
*1.1 从C到C++ 计算机诞生初期,人们要使用计算机必须用机器语言或汇编语言编写程序. 世界上第一种计算机高级语言诞生于1954年,它是FORTRAN语言.先后出现了 多种计算机高级语言.其中使用最广泛.影响最大的当推BASIC语言和C语言 . BASIC语言是1964年在FORTRAN语言的基础上简化而成的,它是为初学 者设计的小型高级语言. C语言是1972年由美国贝尔实验室的D.M.Ritchie研制成功的.它不是为 初学者设计的,而是为计算机专业人员设计的.大多数系统软件和许多应用软件 都是用C语言编写的. 但是随着软件规模的增大,用C语言编写程序渐渐显得有些吃力了. C++是由AT&T Bell(贝尔)实验室的Bjarne Stroustrup博士及其同事于20世 纪80年代初在C语言的基础上开发成功的.C++保留了C语言原有的所有优点, 增加了面向对象的机制. C++是由C发展而来的,与C兼容.用C语言写的程序基本上可以不加修改 地用于C++.从C++的名字可以看出它是C的超集.C++既可用于面向过程的结 构化程序设计,又可用于面向对象的程序设计,是一种功能强大的混合型的程序设 计语言. C++对C的―增强‖,表现在两个方面: (1) 在原来面向过程的机制基础上,对C语言的功能做了不少扩充. (2) 增加了面向对象的机制. 面向对象程序设计,是针对开发较大规模的程序而提出来的,目的是提高软 件开发的效率. 不要把面向对象和面向过程对立起来,面向对象和面向过程不是矛盾的,而 是各有用途.互为补充的. 学习C++,既要会利用C++进行面向过程的结构化程序设计,也要会利用C++ 进行面向对象的程序设计.本书既介绍C++在面向过程程序设计中的应用,也介 绍C++在面向对象程序设计中的应用. *1.2 最简单C++程序 例1.1 输出一行字符: ―This is a C++ program.‖. 程序如下: #include <iostream> //包含头文件iostream using namespace std; //使用命名空间std int main( ) { cout<<″This is a C++ program.″; return 0; } 在运行时会在屏幕上输出以下一行信息: This is a C++ program. 用main代表―主函数‖的名字.每一个C++程序都必须有一个 main 函数 .main前面的int的作用是声明函数的类型为整型.程序第6行的作用是向操作 系统返回一个零值.如果程序不能正常执行,则会自动向操作系统返回一个非零 值,一般为-1.
基于c++实现自动扫雷小程序源码(带说明介绍)课程大作业.zip 基于c++实现自动扫雷小程序源码(带说明介绍)课程大作业.zip 基于c++实现自动扫雷小程序源码(带说明介绍)课程大作业.zip 【资源说明】 该项目是个人毕设项目源码,评审分达到95分,都经过严格调试,确保可以运行!放心下载使用。 该项目资源主要针对计算机、自动化等相关专业的学生或从业者下载使用,也可作为期末课程设计、课程大作业、毕业设计等。 具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现类似其他功能。 从小就喜欢玩扫雷, 写一个自动扫雷程序探索一下胜率的问题。 一共四个文件,main.cpp, saolei.cpp, autosaolei.cpp, saolei.h saolei.cpp 文件内是扫雷游戏生成地雷,生成相应数字表的内容,这些都在类minta中实现。 方法leclick()模拟鼠标左键点击。 方法reclick()模拟鼠标右键点击。 方法findwarn()模拟鼠标左右键一同点击。 autosaolei.cpp 内主要是类autosaolei的实现,具体为通过restart()方法刷新状态,通过working(minta)方法接收一次游戏, 该函数返回值为该次扫雷游戏是否胜利(点开所有非雷格子)。 通过以下具体函数实现 方法working()为主工作循环。 方法randclick()为无法判断后的随机点击。 方法trywarn()为简单逻辑判断,左右键一同点击每个周围有未点开格子的已知格子。若点开的格子上标的雷数与周边已预警和未点开格子一样多。 则点开周边所有未点开格子。当仅适用这一种判断方法时可以以一定概率通过低级,较低概率通过中级,不能通过高级。 方法smartjudge()为复杂逻辑判断,可以判断形如121 或212 等可通过逻辑判断出具体地雷位置的情况。 具体通过"断言"机制,每当trywarn()方法无法点开新的格子,就通过getasserts()方法生成"断言"。每个周围有未点开格子的格子生成一个"断言"。 每个"断言"携带该格子数字减去周围已预警雷数及周围未点开格子位置信息。通过两两断言间相互判断确定无雷或有雷的格子。 (断言机制本身覆盖了trywarn方法的功能,但是出于效率考虑,仍保留trywarn()方法) 具体每千局简单模式胜率在 80% 至 85% 之间 中等模式胜率在 40% 至 42% 之间 高级模式胜率在 0.6% 至 1.5% 之间 还可以通过对不同区域未知格子的地雷概率进行计算,在随机点击时尽可能点击地雷概率小的位置来提高胜率。
根据引用\[1\]中的内容,C代码较C++代码的执行效率高。因此,C版本的代码在运行效率上可能会更高。然而,这并不意味着所有情况下C代码都比C++代码更高效。在实际情况中,代码的执行效率还受到其他因素的影响,如算法的选择、编译器的优化等。因此,要确定哪个版本的代码在特定情况下运行效率更高,需要进行具体的测试和评估。 #### 引用[.reference_title] - *1* [C++编程中提高程序运行效率的方式(不断更新)](https://blog.csdn.net/qq_33810188/article/details/81746890)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [简单测试下Python、Go和C++运行效率](https://blog.csdn.net/ash062/article/details/125812088)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [[C++] 七种常见排序算法的实现及运行效率对比(算法课实验)](https://blog.csdn.net/m0_62405272/article/details/123228288)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值