文章目录
函数命名建议
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