C 良好实践:非变异函数何时应该请求指针而不是副本?

C 良好实践:非变异函数何时应该请求指针而不是副本?,c,pointers,copy,parameter-passing,conventions,C,Pointers,Copy,Parameter Passing,Conventions,考虑到一个非常简单的数据类型,以及一个将该类型作为参数但不改变它的函数,我试图理解什么时候我想传递一个指向它的指针,而不是简单地复制它。就一般的C良好实践而言 在我看来,复制这样一个轻量级的值对性能没有什么意义,指针可能比普通值更容易引起混淆 例如,请考虑下面的代码(摘自Bob Nystrom的《编纂解释器》): typedef结构{ 标记类型; 常量字符*开始; 整数长度; 内线; }代币; 在下面的代码段中,identiersequal采用Token*类型的参数,而不是纯Token。这可能

考虑到一个非常简单的数据类型,以及一个将该类型作为参数但不改变它的函数,我试图理解什么时候我想传递一个指向它的指针,而不是简单地复制它。就一般的C良好实践而言

在我看来,复制这样一个轻量级的值对性能没有什么意义,指针可能比普通值更容易引起混淆

例如,请考虑下面的代码(摘自Bob Nystrom的《编纂解释器》):

typedef结构{
标记类型;
常量字符*开始;
整数长度;
内线;
}代币;
在下面的代码段中,
identiersequal
采用
Token*
类型的参数,而不是纯
Token
。这可能是有道理的-我们不必复制
令牌

另一方面,
addLocal
采用普通的
Token

就C语言的一般良好实践而言,我试图了解
identifiersEqual
接受指针,而
addLocal
接受副本是否有特殊原因。这两个函数都不会改变值,而且-
Token
的权重也不大

这里有我错过的模式吗,还是这只是偶然?在什么情况下我应该这样或那样决定

静态布尔标识符相等(令牌*a,令牌*b){
如果(a->length!=b->length)返回false;
返回memcmp(a->start,b->start,a->length)=0;
}
静态void addLocal(令牌名称){
如果(当前->本地计数==UINT8\U计数){
错误(“函数中的局部变量太多”);
返回;
}
Local*Local=¤t->locals[current->localCount++];
本地->名称=名称;
局部->深度=-1;
}
静态空隙可宣告(){
如果(当前->范围深度==0)返回;
令牌*名称=&parser.previous;
对于(int i=current->localCount-1;i>=0;i--){
本地*本地=&当前->本地[i];
如果(本地->深度!=-1&&local->深度<当前->范围深度)中断;
if(identifiersEqual(名称和本地->名称)){
错误(“此范围中已声明具有此名称的变量”);
}
}
addLocal(*名称);
}

这个问题几乎是在征求意见,所以我应该避免说我可能不会在同一个程序中使用这两个接口,因为我试图避免API,其中数据类型有时通过值传递,有时通过引用传递。但这只是我的问题,所以我将把答案的其余部分限制在一个原因上,为什么您可能选择使用一个按值传递中等大小对象的接口。如果你想听听原始程序员选择这种风格的原因,你应该直接问他

第一点是,如果大多数现代编译器能够访问被调用函数的主体,并且被调用函数本身足够轻量级,可以内联,那么它们将能够避免复制。这些条件似乎适用于引用代码中的函数,因此使用按值调用可能没有成本。[注1]因此,如果API样式为代码读取器提供了有用的信息,则可以认为它是有用的

现在考虑参数原型<代码> x const *<代码>和<代码> x>代码>。在这两种情况下,我们都知道传递的参数不会被修改,所以我们当然不需要复制它

但仍有人担心这场争论的持续时间。如果被调用函数获取一个指针并将该指针保存到一个比调用更久的对象中,那么我们需要担心传递的对象的所有权。实际上,我们需要将对象的所有权传递给被调用的函数,并且还需要确保对象没有自动生存期。特别是,我们不能用临时变量调用函数,我们可能会对用静态生存期(不能是
free
d)的对象调用函数感到怀疑

另一方面,按值调用显然不会对调用方施加任何要求。如果被调用的函数想要保存传递的对象,它负责创建副本,并在副本不再有用时处理副本。我们可以传递任何我们喜欢的对象:临时对象、静态对象或本地对象,这些对象将在后续调用中重用

实际上,令牌对象通常是在解析循环中重用的本地对象,而不是强加更复杂内存管理机制的动态分配对象。大多数情况下,传递给函数的令牌对象只会被查询,但有时确实需要保存它们。函数名
addLocal
强烈建议此函数将持久化传递的对象

在这种特殊情况下,
addLocal
实际上保存了传递的对象,但它保存了一个副本。否则它无法执行此操作,因为它将被传递一个副本,并且该副本将不会在调用期间过期。幸运的是,优化程序几乎肯定会内联
addLocal
,从而避免不必要的中间副本。因此,这里使用的按值调用准确地传达给代码读取器,完全不需要担心传递给
addLocal
的对象的生存期

identifiersEqual
的情况下,被调用的函数似乎不需要持久化任何一个传递的对象,因此保证可能不那么重要。但是,如上所述,为了一致性,我可能也会将
identifiersEqual
编写为按值调用,希望编译器能够成功地完全避免复制。(这是信仰的飞跃,我对一致性的追求可能是一种抽搐。)


笔记
  • 使用非常轻量级的对象和证书