最近在学习《程序员的自我修养——链接、装载与库》时,get到了一个新的知识点:弱符号与弱引用。书中简短的介绍,让我了解到弱符号的含义以及使用方式。了解我的朋友,应该知道我喜欢将知识点与我们实际工作结合起来,在工作中利用起来,正所谓学以善用。根据我的理解,觉得利用弱符号的特性可以帮组我们在工作中编写出更加稳定,可复用,可组合的优秀代码。在此向大家分享。
符号重定义错误
在编码过程中,我们经常遇到符号重定义的错误。编译器会报如下错误:
multiple definition of `xxx';
这就是符号重复定义导致的,再往细里面说,是在同一作用域内符号冲突。我们知道变量是由作用域和生命周期概念的。比如:
例1:
main.c
int strong=0;
int main()
{
printf("strong = %d\n",strong);
return 0;
}
strong.c
int strong=1;
gcc main.c strong.c -o main 则会报重定义错误。因为在main.c 和strong.c 文件中,整型变量strong是全局变量,它们的作用域都是跨文件的。若是在不同的作用域,即使相同变量名,也不会报错。编译器会有默认的优先级处理:总是更小作用域的变量覆盖更大作用域的变量,前提是这两个变量的作用域是包含或被包含的关系。比如:
例2:
main.c
int strong=0;
int main()
{
printf("strong = %d\n",strong);
return 0;
}
strong.c
static int strong=1;
gcc main.c strong.c -o main 不再报错。此时main.c 中的strong 变量的作用域是跨文件,而strong.c中的strong变量的作用域仅限strong.c文件。因此不存在相同作用域中,符号重定义问题。并且结果输出为0; 同理,下面的代码会编译报错吗?输出为多少呢?
main.c
int strong=0;
int main()
{
int strong=2;
printf("strong = %d\n",strong);
return 0;
}
strong.c
static int strong=1;
甚至于下面的代码也是合法的:
main.c
int strong=0;
int main()
{
int strong=2;
if(1)
{
int strong=3;
}
{
int strong = 4;
printf("strong = %d\n",strong);
}
return 0;
}
在C语言中,我们可以简单地认为花括号是文件内作用域的分隔符。
强符号与弱符号
编译器默认函数和已初始化的全局变量为强符号,而未初始化的全局变量为弱符号;同时开发者可以通过"attribute((weak))"来声明一个符号为弱符号; gcc 在编译过程中,对于强弱符号遵循一定规则进行取舍:
- 当有多个为强符号时,报"redefinition of ‘xxx’"错误
- 当仅有一个为强符号时,选取强符号的值
- 当都为弱符号时,选择其中暂用空间较大的符号。(防止溢出越界等问题)—— 这个应该和编译器有关,我在本地环境中,是不允许多个类型不同的弱符号存在的。编译会出错。
很明显,例1 则是出现多个强符号,导致的redefinition 错误。例如下面code:
main.c
int strong;
int strong=2;
int main()
{
printf("strong = %d\n",strong);
return 0;
}
编译并不会出错,并且输出为2;
强引用与弱引用
我们知道在编译成可执行文件时,若源文件引用了外部目标文件的符号,在链接过程中,需要找到对应的符号定义,若未找到对应符号(未定义),链接器会报符号位未定义错误,导致编译出错。这种被称为强引用。与相对应的时弱引用(开发者可通过attribute((weakref))声明),链接器在链接符号过程中,若发现符号为弱引用,即使没有找到符号定义,链接时也不会报错,但是会将该引用默认为0; 书中的代码如下:
main.c
__attribute__((weakref)) void foo(void);
void main(void)
{
foo();
}
虽然没有定义foo(),但是我们可以将它编译成可执行文件,并且GCC 编译不会报链接错误,但是当我们运行时,会发生运行错误。实际上新版本的编译器上诉的代码会在链接时报错的,新版本的示例代码应该如下:(新版本的weakref 需要函数别名,且必须是static 修饰)
main.c
static __attribute__((weakref("foo"))) void myfoo(void);
void main(void)
{
if(myfoo)
{
myfoo();
}
}
新版的弱符号引用如上所示。即表示若没有找到foo函数,编译不报错,但是会默认myfoo为NULL。若在其他库中定义了foo函数,对myfoo的引用就相当于对foo的引用。这种弱引用在库的使用上十分有用的。
小节
经过上面的描述,我们了解到了强符号,弱符号,强引用,弱引用的概念。我认为起码有两点特性可以在我们工作中使用:
- 强符号可以替换弱符号。
- 弱引用可以避免函数未定义的错误。
强符号替换弱符号
一些库中对外接口可以声明为弱符号。比如: 在math库中,我们发现add(int num1, int num2) 这个接口存在问题,那我们解决方式一般有以下几种: 1. 实现一个myadd(int num1,int num2) 接口,之后再将项目中的所有add ,替换为myadd 。这种方式可行,但是存在缺点:修改量大,并且后续人员不清楚背景,很有可能继续使用熟悉的add 接口。 2. 更新math库,从更本解决此问题。这种方式比较推荐。但是也并不是通用的,比如有些库并不是开源的,并且已经过了支持日期,也就不适用了。
此时,我们可以自己在项目中定义一个add(int num1,int num2) 接口,用强符号替换库中的弱符号,这样改动是比较小的。(这种情景需要了解接口的实现内容,可给调用者较高的重构权力)
巧用弱引用提高代码的健壮性
应用层的开发,离不开sdk的提供,一般sdk维护了,即使应用没有需求发生,往往也会为了配合sdk,进行简单的修改。以设备升级作为举例,若升级过程中,分为传包(pass),验签(verify),解密(decode),安装(install),上传日志(report)等步骤,并且这些核心接口都是以libsdk.so库的形式提供给应用工程师。那么正常情况下,应用逻辑大致如下:
用户业务流程
...
pass();
...
verify();
...
decode();
...
install();
...
report();
...
但是这样的业务代码,我觉得是非常差的。比如新的项目中,不需要做解密包操作了,(理论上libsdk.so库中应该不具备decode接口了),这样就会导致应用程序编译失败。undefine 'decode' 。 因此我建议应用代码可以如下:
static __attribute__((weakref("pass"))) void mypass(void);
static __attribute__((weakref("verify"))) void myverify(void);
static __attribute__((weakref("decode"))) void mydecode(void);
static __attribute__((weakref("install"))) void myinstall(void);
static __attribute__((weakref("report"))) void myreport(void);
用户业务流程
...
if(mypass)
mypass();
else
printf("don't need pass\n");
...
if(myverify)
myverify();
else
printf("don't need verify\n");
...
if(mydecode)
mydecode();
else
printf("don't need decode\n");
...
if(myinstall)
myinstall();
else
printf("don't need install\n");
...
if(myreport)
myreport();
else
printf("don't need report\n");
...
以上便是我理解的内容,希望能引起的你的共鸣,如果你有好的想法,也可以和我一起分享。。。
|