浅谈C语言中的强符号、弱符号、强引用和弱引用

首先我表示很悲剧,在看《程序员的自我修养--链接、装载与库》之前我竟不知道C有强符号、弱符号、强引用和弱引用。在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样感觉的朋友交流也希望高人指点。

  首先我们看一下书中关于它们的定义。

  引入场景:(1)文件A中定义并初始化变量i(int i = 1), 文件B中定义并初始化变量i(int i = 2)。编译链接A、B时会报错b.o:(.data+0x0): multiple definition of `i';a.o:(.data+0x0): multiple definition of `i'。(2)在文件C中定义并初始化两个变量i(int i = 1; int i = 2), 编译链接时会报错c.c:2:5: error: redefinition of ‘i'; c.c:1:5: note: previous definition of ‘i' was here。

  强符号:像场景中这样的符号定义被称为强符号,对于C/C++来说,编译器默认函数和初始化的全局变量为强符号。
  弱符号:接上文,为初始化的全局变量为弱符号。
  编译器关于强弱符号的规则有:(1)强符号不允许多次定义,但强弱可以共存;(2)强弱共存时,强覆盖弱;(3)都是弱符号时,选择占用空间最大的,如选择  double类型的而不选择int类型的。

  由以上定义所以有我之前没有想到的场景:
  代码a.c:

1 int i = 2;
  代码b.c:

代码如下:

#include<stdio.h>

int i;
int main(int argc, char** argv)
{
      printf("i = %d\n", i);
      return 0;     
}

  编译文件a和b并链接,结果输出i为2而不是0。
  并且在同一个文件中定义但未初始化两个相同的变量不会报错,只有在使用变量时才会报错。
  对于GCC编译器来说,还允许使用__attribute__((weak))来将强符号定义为弱符号,所已有
  代码c.c

代码如下:

#include<stdio.h>
 
  __attribute__((weak)) int i = 1;
 
  int main(int argc, char** argv)
  {
       printf("i = %d\n", i);
       return 0;  
  }

  结果i的输出仍未2而不是1。

  那么对于函数而言是不是也这样呢?先不看函数,而是先看由强弱符号而进一步引入的强弱引用。书中关于强弱引用的概述是对于强引用若未定义则链接时肯定会报错,而对于弱引用则不会报错,链接器默认其为0(这一点对于函数好理解,即函数符号所代表入口地址为0;对于变量就要注意了,既然是引用那自然就是地址了,所以同函数一样变量的地址为0而不是变量的值为0)。此时对于强弱引用是不是还没有什么明确的概念呢?到底什么是引用?引用和符号又是什么关系?这里我说一下我的理解(欢迎指正),在定义和声明处指定的函数名、变量名即为对应的符号,而在代码其他处调用函数或使用变量时,则把函说明和变量名看作引用,这样一来符号和引用在代码层面上其实就是一个东西,只是根据环境而叫法不同而已。那么强符号对应强引用,弱符号对应弱引用。

  有上面的强弱引用的特点可看出,当一个函数为弱引用时,不管这个函数有没有定义,链接时都不会报错,而且我们可以根据判断函数名是否为0来决定是否执行这个函数。这样一来,包含这些函数的库就可以以模块、插件的形式和我们的引用组合一起,方便使用和卸载,并且由于强符号可以覆盖弱符号和强弱符号与强弱引用的关系可知,我们自己定义函数可以覆盖库中的函数,多么美妙。

  先看根据条件判断是否执行函数:
  代码d.c

代码如下:

#include<stdio.h>
 
void func()
{
     printf("func()#1\n");
}

  代码e.c

代码如下:

#include<stdio.h>
 
 __attribute__((weak)) void func();
 
 int main(int argc, char** argv)
 {
      if (func)
          func();
      return 0;
 }

  编译d.c,cc -c d.c 输出d.o;编译e.c并链接d.o,cc d.o e.c -o e输出可执行文件e,运行e正常执行函数func。编译e.c但不链接d.o,此时并不会报错,只不过func不会执行,因为没有它的定义所以if(func)为假。
  再看函数覆盖:
  代码f.c

代码如下:

#include<stdio.h>
 
 __attribute__((weak)) void func()
 {
      printf("func()#1\n");
 }

  代码g.c

代码如下:

#include<stdio.h>
 
 void func()
 {
      printf("func()#2\n");
  }
 
 int main(int argc, char** argv)
 {
      func();
      return 0;
 }
 ~

  编译链接,结构输出"func()#2"。

  以上可以说明函数和变量是保持一致的,其实对应变量也可以像使用函数那样先判断再使用,只不过不是判断变量的值而是变量的地址,如
  代码v1.c

代码如下:

int i = 2;

  代码v2.c

代码如下:

#include<stdio.h>
 
 __attribute__((weak)) extern int i;
 
 int main(int argc, char** argv)
 {
      if (&i)
          printf("i = %d\n", i);
     return 0;
 }
 ~

  编译并链接v1时,输出2;编译但不链接v1时无输出。这样做时要分清定义和声明的区别,__attribute__((weak)) int i 是定义变量并转换为弱符号,这样i是分配了空间的,而__attribute__((weak)) extern int i 则将原来定义的变量i由强符号转换为弱符号,导致使用i时不是强引用而是弱引用。不过虽然变量可以这么做但没有函数那样有意义。

  上面关于强弱引用仍旧使用的是GCC提供的__attribute__((weak)),而书中还提到了__attribute__((weakref)),后者貌似更能体现“引用”这一关键词。而我之所以使用前者来介绍强弱引用,是因为我对关于强弱符号与强弱引用对应关系的理解。关于__attribute__((weakref))的使用方法,这里介绍一种(两者都有不同的使用方法)。
  代码a.c

代码如下:

#include<stdio.h>
 
 void bar()
 {
      printf("foo()\n");
 }

  代码b.c

代码如下:

#include<stdio.h>
 
 static void foo() __attribute__((weakref("bar")));
 
  int main(int argc, char** argv)
 {
      if (foo)
         foo();
 
      return 0;
 }

  注意函数foo的static修饰符,没有的话会报错,这样将函数foo限制在只有本文件内可使用。

  好了,夜已深,写的有点凌乱,我也凌乱了。

(0)

相关推荐

  • 新手小心:c语言中强符号与弱符号的使用

    声明:下面的实例全部在linux下尝试,window下未尝试.有兴趣者可以试一下.文章针c初学者.c语言的强符号和弱符号是c初学者经常容易犯错的地方.而且很多时候,特别是多人配合开发的程序,它引起的问题往往非常行为怪异而且难以定位.什么是强符号和弱符号?在c语言中,函数和初始化的全局变量是强符号,未初始化的全局变量时弱符号.强符号和弱符号的定义是连接器用来处理多重定义符号的,它的规则是:不允许多个强符号:如果一个强符号和一个弱符号,这选择强符号:如果多个弱符号,则任意选一个.它的陷阱:上代码:

  • 详解C语言中的符号常量、变量与算术表达式

    C语言中的符号常量 在结束讨论温度转换程序前,我们再来看一下符号常量.在程序中使用 300.20 等类似的"幻数"并不是一个好习惯,它们几乎无法向以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难.处理这种幻数的一种方法是赋予它们有意义的名字.#define 指令可以把符号名(或称为符号常量)定义为一个特定的字符串: #define 名字 替换文本 在该定义之后,程序中出现的所有在 #define 中定义的名字(既没有用引号引起来,也不是其它名字的一部分)都将用相应的替换文本

  • 深入解读C语言中的符号常量EOF

    EOF是指文件的结束符,是一个宏定义     借助于getchar 与putchar 函数,可以在不了解其它输入/输出知识的情况下编写出 数量惊人的有用的代码.最简单的例子就是把输入一次一个字符地复制到输出,其基本思想 如下: 读一个字符 while (该字符不是文件结束指示符) 输出刚读入的字符 读下一个字符 将上述基本思想转换为C语言程序为: #include <stdio.h> /* copy input to output; 1st version */ main() { int c;

  • C语言中无符号数和有符号数之间的运算

    C语言中有符号数和无符号数进行运算(包括逻辑运算和算术运算)默认会将有符号数看成无符号数进行运算,其中算术运算默认返回无符号数,逻辑运算当然是返回0或1了. unsigned int和int进行运算 直接看例子来说明问题吧 #include <iostream> using namespace std; int main() { int a = -1; unsigned int b = 16; if(a > b) cout<<"负数竟然大于正数了!\n";

  • C语言中的强符号和弱符号介绍

    之前在extern "C" 用法详解中已经提到过符号的概念,它是编译器对变量和函数的一种标记,编译器对C和C++代码在生产符号时规则也是不一样的,符号除了本身名字的区别外,还有强符号和弱符号之分 我们先看一段简单的代码 复制代码 代码如下: /* test.c */  void hello();  int main()  {      hello();      return 0;  } 很显然,这段代码是没法链接通过的,它会报错undefined reference to hello

  • 举例讲解C语言链接器的符号解析机制

    1. 符号分类 (1)全局符号:非静态全局变量,非静态函数 (2)外部符号:定义于其它模块,而被本模块引用的全局变量和函数 (3)本地符号:静态变量(包括全局和局部),静态函数 对于静态局部变量,编译器会为其生成唯一的名字.如x.fun1,x.fun2.本地符号对链接器来说是不可见的. 2. 符号决议 当编译器遇到一个不是本模块定义的符号时,会假设该函数由其它模块定义,并生成一个链接器符号表条目,交由链接器处理.如果链接器在它的任何输入模块都没有找到该符号,会给出一个类似undefined re

  • 浅谈c语言中类型隐性转换的坑

    谨记:在C语言中,当两种不同类型之间运算时,低字节长度类型会向高自己长度类型转换,有符号会向无符号类型转换. 举例子如下: #include <stdio.h> void func(void) { int i = 1; unsigned char c1 = 1; signed char c2 = -1; if (c2 > i){ printf("\r\n -1 > 1"); } else{ printf("\r\n -1 <= 1");

  • 浅谈C语言中的注释风格小结

    C语言中常用的注释风格有两种,一种是通过如下模式进行一段代码的注释: /* comment*/ 另一种是单行注释符号: // comment 学生时代的注释我一般是选用后者,那时候编码量十分有限,即使是简单的小段落注释使用的IDE也支持批量添加单行注释符.而在编码之中,简单的单行注释进行注释的时候键盘的操作更为简单一点. 不过,工作之后接触了相应的编码规范之后,C语言的注释我基本上放弃了单行注释的方法,最多仅仅在调试的时候做简单的使用. 其实,单行注释是从C++中借鉴来的,算是C++风格的注释方

  • 浅谈Go语言中的结构体struct & 接口Interface & 反射

    结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struct类型理解为类,可以定义方法,和函数定义有些许区别: struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User var user1 *User = &User{} var user2 *User = new(User) struct使用 下面示例中user1和

  • 浅谈c语言中一种典型的排列组合算法

    c语言中的全排列算法和组合数算法在实际问题中应用非常之广,但算法有许许多多,而我个人认为方法不必记太多,最好只记熟一种即可,一招鲜亦可吃遍天 全排列: #include<stdio.h> void swap(int *p1,int *p2) { int t=*p1; *p1=*p2; *p2=t; } void permutation(int a[],int index,int size) { if(index==size) { for(int i=0;i<size;i++) print

  • 浅谈c语言中转义字符的用法及注意事项

    c语言中的转义字符: \a 响铃符 \b 退格 \f 换页符 \n 换行符 \r 回车符(回到该行的首位置) \v 纵向制表符 \\ 反斜杠 \? 问号(?经vs10测试可以直接打印) \"(\') 双引号(单引号) \ooo 八进制数(ooo表示一个用8进制数表示出来的对应ANSII代码对应出字符,用此方法可以表示出所有ASCII字符.不过测试发现打不出%号,存疑!) \xhh 十六进制数(功能同八进制数,用hh表示一个十六进制数,如\x20表示空格) 注:使用转义字符的退格符,换行符,回车符

  • 浅谈C语言中的强符号、弱符号、强引用和弱引用

    首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样感觉的朋友交流也希望高人指点. 首先我们看一下书中关于它们的定义. 引入场景:(1)文件A中定义并初始化变量i(int i = 1), 文件B中定义并初始化变量i(int i = 2).编译链接A.B时会报错b.o:(.data+0x0): multiple definition of `i':a.o:(.d

  • 浅谈Go语言中的次方用法

    Go语言中符号 " ^ " 不再用于次方,而是表示"按位异或的运算" 具体的运算规则如下: 按位异或 ^ : 两位一个为 0, 一个为 1 ,结果为 1 ,否则为 0(位表示二进制的机器码) 例子如下: 所以Go语言中2^3 = 1 不是 8(注意:计算机都是按照补码进行运算) 那么Go语言中的次方是什么:(下图所示) (官方文档是个好帮手) 补充:leetcode golang实现一个数的整数次方 pow(x, n) 我就废话不多说了,大家还是直接看代码吧~ pa

  • 浅谈Go语言中字符串和数组

    go语言里边的字符串处理和PHP还有java 的处理是不一样的,首先申明字符串和修改字符串 复制代码 代码如下: package main import "fmt" var name string           //申明一个字符串 var emptyname string = "" //申明一个空字符串 func main() {  //申明多个字符串并且赋值  a, b, v := "hello", "word", &

  • 浅谈C语言中结构体的初始化

    <代码大全>建议在变量定义的时候进行初始化,但是很多人,特别是新人对结构体或者结构体数组定义是一般不会初始化,或者不知道怎么初始化.1.初始化 复制代码 代码如下: typedef struct _TEST_T {        int i;        char c[10];}TEST_T;TEST_T gst  = {1, "12345"};//可以初始化,设置i为1,s为一个字符串.TEST_T gst  = {1};//初始化个数少于实际个数时,只初始化前面的成员

  • 浅谈C语言转义字符和格式控制符

    转义字符参考: \a:蜂鸣,响铃 \b:回退:向后退一格 \f:换页 \n:换行,光标到下行行首 \r:回车,光标到本行行首 \t:水平制表 \v:垂直制表 \\:反斜杠 \':单引号 \":双引号 \?:问号 \ddd:三位八进制 \xhh:二位十六进制 \0:空字符(NULL),什么都不做 注: 1,\v垂直制表和\f换页符对屏幕没有任何影响,但会影响打印机执行响应操作. 2,\n其实应该叫回车换行.换行只是换一行,不改变光标的横坐标:回车只是回到行首,不改变光标的纵坐标. 3,\t 光标向

随机推荐