modern_cpp_4-C++ Functions
2022/1/23 17:34:21
本文主要是介绍modern_cpp_4-C++ Functions,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章目录
- 函数命名建议
- 函数返回值
- 返回类型自动推导和返回多个值
- RVO(Return Value Optimization)
- 局部变量和静态变量
- 默认参数
- 传递较大的参数应使用`Const Reference`
- 实验:Cost of passing by value
- inline
- Overloading
- Naive overloading
- Good Practices & Bad Practices
- 实践
- Namespaces
函数命名建议
Google-Style使用驼峰法命名函数,例如CamelCase
,并且更加倾向于将单个函数做的更小一些。
函数返回值
关于返回值,If has a return value, must return a value. If returns void, must NOT return any value.
返回类型自动推导和返回多个值
但是在C++14中,支持返回类型自动推导(automatic return type deduction),例如
std::map<char,int> GetDictionary(){ return std::map<char,int>{{'a',27},{'b',3}}; }
可以被表达成
auto GetDictionary(){ return std::map<char,int>{{'a',27},{'b',3}}; }
但还是不能和Python那样返回多个值灵活:
#!/usr/bin/env python3 def foo(): return "Super Variable",5 name,value = foo() print(name+"has value: "+str(value))
我们尝试使用C++17中的新特性(Structured binding,元组)模仿
#include<iostream> #include<tuple> auto Foo(){ return std::make_tuple("Super Variable",5); } int main(){ auto [name,value] = Foo(); std::cout<<name<<"has value: "<<value<<std::endl; return 0; }
但是千万要注意,不要返回局部变量的引用,因为局部变量会随着被调用函数结束而被清理,下面的这种做法是错误的:
#include <iostream> using namespace std; int & MultiplyBy10(int num){ int retval = 0; cout<< "retval is "<<retval <<endl; return retval; }// retval is destroyed, it's not accesisble anymore int main(){ int out = MultiplyBy10(10); cout<< "out is "<<out<<endl; // not 0 return 0; }
RVO(Return Value Optimization)
C++针对Return Value还会进行优化,可以参考Copy elision - Wikipedia
Copy Elision指一个编译器优化技巧,即消除不必要的对象的复制,其中一个重要体现就是RVO(Return Value Optimization)。
C++中有一个假设原则(as-if rule),即C++标准允许编译器执行任何程度的优化,只要生成的可执行文件表现出相同的可观察行为。而RVO指的是C++中的一个特殊语句,比假设原则更进一步,即使拷贝复制函数有作用,实际操作中也可以忽略由return语句产生的复制操作。
比如下面的程序:
#include <iostream> struct C { C() = default; C(const C&) { std::cout << "A copy was made.\n"; } }; C f() { return C(); } int main() { std::cout << "Hello World!\n"; C obj = f(); }
这里按理说,应该会执行两次拷贝复制函数,即输出两次A copy was made.
,第一次是f()
内部将一个未命名的临时C
复制拷贝给Return Value,第二次是main
内部的f
到obj
,但是结果有点出乎意料:
实际中,根据编译器的优化不同,下面三种情况都有可能会发生
Hello World! A copy was made. A copy was made.
Hello World! A copy was made.
Hello World!
我们的编译器在这里什么也没做,直接在main
函数中创建新的C
,确实是最简洁的实现,这种方法是被Walter Bright第一次在他的Zortech C++ compiler
中实现的,而目前的绝大多数的C++编译器都支持了RVO。
局部变量和静态变量
除非声明全局静态变量,否则每次触发局部变量的定义,都会创建一份局部变量:
void f(){ float var1 = 5.5F; float var2 = 1.5F; } f(); // First call,var1,var2 are created f(); // Second call,NEW var 1,var2 are created
NACHO-STYLE: 如果可能的话,请避免使用静态变量,下面的例子会介绍一种微妙的使程序崩溃的方法,即”initialization order ’fiasco‘ “(静态变量的初始化顺序),这种错误很难觉察到,而且发生在main
函数开始之前。可以参考这篇文章:https://isocpp.org/wiki/faq/ctors#static-init-order
在C++标准 ‘ISOIEC14882-1998 3.6.2 Initialization of nonlocal objects’ 里明确规定,在同一个编译单元内,静态变量初始化的顺序就是定义的顺序,而跨编译单元的静态变量的初始化顺序未定义,具体的初始化顺序取决于编译器的实现。所以如果跨编译单元的定义使用静态变量,有可能所调用的静态变量还没有初始化,这是十分危险的。
当然静态变量的初始化分为静态初始化和动态初始化,前者可以认为是同步完成的,后者就会因为跨编译单元而导致初始化顺序不确定。
防范措施:尽量避免跨编译单元的初始化依赖,常用construct on first use idiom,即将静态变量定义在函数内,直到函数被调用才会被初始化,这就消除了跨编译单元的静态变量的初始化问题。
默认参数
例子:
#include<iostream> using namespace std; string SayHello(const string& to_whom = "world"){ return "Hello"+ to_whom +"!"; } int main(){ // Will call SayHello using default arguments cout<<SayHello()<<endl; // This will override the default argument cout<<SayHello("students")<<endl; return 0; }
但是默认参数只能够在定义中设置,不能在声明中设置,一方面简化了程序,另一方面会造成使用者在调用时,忽略已经设置的默认参数,当在同一个作用域内,再次设置默认参数时会造成意料之外的行为:
// 下面的代码在编译时会出错 #include <iostream> using namespace std; void func(int a, int b = 10, int c = 36); int main(){ func(99); return 0; } void func(int a, int b = 10, int c = 36){ cout<<a<<", "<<b<<", "<<c<<endl; }
如果把当前函数的定义和声明分开,仍然设置两次默认参数,就不会出现这个问题了(但是我还是出现了问题,这好像应该就是一个作用域吧,日后可以再琢磨琢磨: C++函数的默认参数详解)。
总结:
在给定的作用域中,一个形参只能被赋予一次默认实参,换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。通常应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
GOOGLE-STYLE规定,只有当可读性更好的时候才能使用默认参数
NACHO-STYLE规定,不应该使用默认参数
更多参考:到底在声明中还是定义中指定默认参数 (biancheng.net)
传递较大的参数应使用Const Reference
当传递的参数较大时,函数直接拷贝可能会比较耗时,此时应该传递引用。
而且如果可以的话,应该尽量使用常量引用(尽管再C++11之前,非常量引用是很常见的),GOOGLE-STYLE中提出了避免使用non-const refs.
实验:Cost of passing by value
这里针对值传递和引用传递进行了性能的对比,后者要比前者快五倍。
inline
内联函数是用来提醒编译器,针对该部分 should attempt to generate code for a call rather than a function call,当然编译器会进行决断,如果这些代码块足够小,编译器会进行优化。
例如下面的阶乘:
inline int fac(int n){ if(n<2) return 2; return n*fac(n-1); }
Overloading
Naive overloading
在标准库中的数学计算库cmath
中,简单的余弦和正切都会按照输入参数的类型而在命名上产生差异:
#include<cmath> double cos(double x); double cosf(float x); long double cosl(long double x);
但是在实际中我们只用使用cos
就可以了,这里就是实现了重载:
#include<cmath> double cos(double x); double cos(float x); long double cos(long double x);
编译器会根据输入的参数类型推断应该采用哪个函数(注意,不是根据返回值的类型推断!)
GOOGLE-STYLE:避免不明显的重载。
Good Practices & Bad Practices
- 将复杂的计算拆解成模块更小,意义更加紧凑的子模块
- 函数的长度应该越小越好
- 避免无意义的注释
- 一个函数应该只承担一项任务
- 如果你不能去一个简单的名字,那就用它的功能命名它
实践
// Good Practice #include<vector> #include<iostream> using namespace std; vector<int> CreateVectorOfZeros(int size); int main(){ vector<int> zeros = CreateVectorOfZeros(10); return 0; }
Namespaces
命名空间能够防止命名上的冲突和从逻辑上可以更加容易管理程序,下面的例子简单展示了如何使用Namespace:
#include<iostream> namespace fun{ int GetMeaningOfLife(void){ return 42; } } namespace boring{ int GetMeaningOfLife(void){ return 0; } } int main(){ std::cout<<boring::GetMeaningOfLife()<<std::endl; std::cout<<fun::GetMeaningOfLife()<<std::endl; }
有时候,我们可以利用{}
来隔绝定义域,在小范围内使用某种命名空间:
#include<cmath> #include"my_pow.hpp" int main(){ int std_result = std::pow(2,3); int my_result = my_pow::pow(2,3); { using std::pow; result = pow(2,3); // same as std::pow } { using my_pow::pow; result = pow(2,3); //same as my_pow::pow } }
记住:永远不要在*.hpp
中使用using namespace name
GOOGLE_STYLE: 如果你发现自己依赖于某些内容,而且这些内容不应该在其他文件中被看到,你就要在当前文件的开头把它们放进namespace
更多参考 https://google.github.io/styleguide/cppguide.html#Namespaces
这篇关于modern_cpp_4-C++ Functions的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-06-30uniAPP 实现全屏左右滚动滚动的效果-icode9专业技术文章分享
- 2024-06-30如何在本地使用授权或插件-icode9专业技术文章分享
- 2024-06-30伪静态规则配置方法汇总-icode9专业技术文章分享
- 2024-06-29易优CMS安装常见问题汇总-icode9专业技术文章分享
- 2024-06-28易优新手必读安装教程-icode9专业技术文章分享
- 2024-06-28忘记eyoucms后台密码怎么办?-icode9专业技术文章分享
- 2024-06-26终极指南:Scrum中如何设置需求优先级
- 2024-06-26AI大模型企业应用实战(25)-为Langchain Agent添加记忆功能
- 2024-06-26小白家庭 nas 搭建方案-icode9专业技术文章分享
- 2024-06-23AI大模型企业应用实战(14)-langchain的Embedding