公司网站客户案例,泰安营销型网站建设公司,网站百度搜索情况和反链接优化建议,如何做免费域名网站前言
在这里我会简要地介绍产生命名冲突的原因#xff0c;和C中处理命名冲突的方法#xff0c;同时和C语言的解决办法进行比较。 相信你在阅读完之后一定会有收获。对于我们来说#xff0c;了解编译器的编译链接过程才能更好的理解编译器是如何报错的#xff0c;更能让我们…前言
在这里我会简要地介绍产生命名冲突的原因和C中处理命名冲突的方法同时和C语言的解决办法进行比较。 相信你在阅读完之后一定会有收获。对于我们来说了解编译器的编译链接过程才能更好的理解编译器是如何报错的更能让我们把持细节。 其中有*的部分是一些补充内容。
1. 编译链接
实际上我们所写的代码都会经历一个翻译链接的过程。 计算机是无法识别我们所写的代码的计算机只能识别二进制的指令所以我们所写的代码需要经过翻译才能被计算机识别。这个翻译就是编译所做的事情。由于我们写代码的时候大可能是由多个源文件组成的一个程序需要将多个源文件联系起来就是链接的过程了最后生成可执行程序。 注意多个源文件是分开进行的编译 其中编译分为如下阶段 预编译 编译 汇编
然后进行多个文件的链接大致流程如图 大致一个项目的过程如下
1.1 编译环节
下面会大致介绍一下编译过程会发生的事。 预编译。在预编译中我们的编译器会干啥事呢 a、头文件展开 #include b、宏的展开 #define c、消除注释 …… 在Windows环境下经过预编译的文件会生成一个后缀为 .i 的文件在Linux环境下经历预编译的环境下生成的后缀为.i的文件 编译。在编译的时候编译器要干的事就很多了 a、词法分析。这个步骤就是来识别“单词”的例如int a; 就会被识别为“int”关键字“a”标识符…… b、语法分析。简单来说就是检查语法是否正确的。 c、语义分析。重点是检查语法结构是否正确例如int num “hehe”这样类型不匹配的语义错误。 d、符号表管理。符号表是一种数据结构用于记录源文件中出现的全局的变量名、函数名等等这个符号表后面还会用到。 在Windows环境下经过编译环境过后会生成一个后缀为.asm的文件在Linux环境下经历编译过后会生成一个后缀为.s的文件。 汇编。在这个环节中编译器会将代码翻译为计算机能识别的二进制指令。然后再进行下一步的处理但是在这个时候我们的符号表内还有一些外部符号需要处理例如一个外部函数或者变量等等等会处理的主题还需要后面的链接过程进行进一步的处理。 在Windows环境下经过汇编环境过后会生成一个后缀为.obj的文件在Linux环境下经历汇编过后生成一个后缀为.o的文件。 1.2 链接环节
链接过程涉及比较复杂过多的不再介绍。我们主要介绍链接环节会进行符号表的合并。那么整个项目中的全局变量和函数都会进入这个符号表那么在刚才的汇编过程中的一些外部符号就得到处理了。 下面来举个例子来说明
/* add.c文件 */
int Add(int x, int y)
{return x y;
}
// .../
/* test.c文件 */
int Add(int x, int y); //函数的声明int main()
{Add(1, 2);return 0;
}在上面的例子中我们在add.c文件中定义了函数Add在test.c文件中声明了函数Add。 上面图片函数地址是假设的 做个比喻声明就好像是一个承诺定义就是兑现承诺。编译过程的不会检查函数是否是实现的它只会检查是否是承诺过的声明直到后面的链接过程后就会将实现的函数的地址汇总进完整的符号表函数承诺就相当于获得了兑现。1、那么如果我们使用了未定义的变量或者函数那会发生什么呢很显然在我们刚刚讲过的1.1编译环节就谈到了会进行语法分析、语义分析所以当我们使用了未定义的变量或者函数名时就是编译环节就会报错。2、那么如果我们声明了变量或者函数但是没有定义那会发生什么呢在刚刚的链接环节已经介绍了声明的外部符号会在完整的符号表中去寻找如果找不到有效地址那么就会出现链接错误。3、那么如果我们重定义了变量或者函数呢那么在符号表中就会用同名的函数但是它们的有效地址却不相同对于编译器来说就无法分辨调用哪一个变量或者函数了。这就是我们接下来要讨论的重点命名冲突。
2. 处理方式
在介绍C的处理方式之前我们先来看C语言的处理方式2.1 C语言static关键字
在C语言中大概就有两种作用域a、全局域 b、局部域。在全局域中的变量和对象是具有外部链接属性的extern声明对于它们来说会有和其它源文件冲突的风险。比如说在一个项目中有人在a.c文件全局定义了Arr变量另一个人在b.c文件全局定义了Arr变量文件名和变量名皆是举例子这种命名风格不可行但是含义完全不同这时候根据我们上面所说编译器就会犯迷糊了就会链接失败。又比如说库里实现了一个函数而程序员又自己写了一个同名的函数同时包含了这个库函数的头文件那么编译器应该听谁的呢……所以在C语言中为了解决命名冲突的问题我们需要使用关键字static
2.1.1 static用法
我们首先回顾一下startic用法 修饰局部变量时。该变量的存储类型从自动存储类型变为静态存储类型即从栈区存储的变量变成了静态区存储的变量。 修饰全局变量时。本来处于全局域的变量是具有外部链接属性的经过static修饰过后该全局变量的链接属性变为内部链接属性即作用域仅局限于本源文件不可被其它源文件访问。 修饰函数。同样地全局函数是具有外部链接属性的经过static修饰过后的函数其只能在定义的源文件中使用对于其它文件来说就是不可访问的。
那么根据第2、3条修饰规则我们可以发现对于C语言来说解决命名冲突的方式就是对于仅仅在本源文件使用的全局变量或者函数或者若干个函数需要共享同一组全局变量可以将这些函数放在同一个源文件中把他们所需要的变量也放在该源文件中并用static修饰即可防止命名冲突。同时注意我们可以全局多定义同名函数但是需要保证的是没有static修饰的函数数量 1,这样才能保证运行成功。 例如
/* 源文件 */
static int g(int x)
{//...
}void func()
{//...g(); //本文件调用g();
}* 2.1.2 extern关键字
说到static关键字就不乏说到extern关键字了这个关键字的作用是声明一个外部对象可以理解为处于其它文件的全局变量 例如
/* add.c文件 */
int global_num 10;//此处声明了一个全局变量global_numint Add(int x, int y)
{return x y;
}
// .../
/* test.c文件 */
int Add(int x, int y); //函数的声明
//如果想在该文件使用该变量的话做如下声明
extern int global_num; //这是一个声明不是定义int main()
{Add(1, 2);return 0;
}extern关键字显示说明了global_num的存储空间是在程序的其它地方分配的。
从编译器的角度来看通过该声明编译器知道会在链接的过程中找到这个变量的定义地方地址。但是对于这个关键字还有很多细节 例如来看下面一个程序
// 说明已在外源文件声明了arr1 和 arr2
/*
int arr1[3] { 0 };
int arr2[3] { 0 };
*/extern int* arr1;
extern int arr2[]; //代码一
extern int arr2[3]; //代码二int main()
{printf(指针类型%zd\n, sizeof(arr1));printf(数组类型%zd\n, sizeof(arr2));return 0;
}该程序运行的结果会让你大吃一惊 这是在VS2022x64坏境下进行的 实际上编译器对于代码一的处理是警告的 甚至是将该[ ]的数字改变了也会影响结果。这样的使用会造成什么结果呢这样的使用会造成许多意想不到的结果。比如说在原定义中声明为了int型但是在外部对象中声明为long在不同环境下由于内存所占的字节不同。这样的话由于两个这样对于其中一个的赋值另一个可能得到不如意的结果。这样的结果是防不胜防的。 这样的结果不是我们所希望的。所以日常使用的过程之中建议就是少用全局变量
2.2 C命名空间
2.2.1 基本使用
我们的C就看到了C语言的命名冲突的问题灵感乍现之下创建了一个新的语法——命名空间。 首先我们先来了解一个常识 在C中有四种域{ }之中的
全局域局部域命名空间域类域
命名空间域就是今天的主题。其中全局域和局部域决定了一个变量的生命周期而我们的类域和命名空间域不会。 根据我们上面的了解我们知道了在同一作用域下同名的变量会发生冲突命名空间域就相当于设置了一个墙防止与全局域中的变量发生冲突。那么我们可以在命名空间域中干什么呢实际上我们可以把它当作另类的全局域。 关键字namespace。在这个命名空间域中我们可以定义任意类型例如内置类型自定义类型函数……
namespace Er
{int b 10;typedef struct STNode{int val;struct STNode* next;}STNode, *PSTNode;int ADD(int x, int y){return x y;}char c *;
}访问方法也很简单操作符 :: 例如
namespace Er
{int a 10;
}
int a 1;int main()
{int a 2;cout a endl; //局部域cout ::a endl;//全局域cout Er::a endl;//命名空间域
}上面的示例给出了a的三种域的访问方式我们可以看到全局域可以通过空命名空间的方式在局部域中访问。实际上对于C标准库定义的命名空间就是std。我们需要使用里面的函数类对象都是需要访问这个命名空间的例如cout、cin我们每次都去声明命名空间域就太过于麻烦了所以我们采用展开命名空间的方式使用其中的变量。这里的展开命名空间又分为两种1、部分展开 2、全部展开。 对于展开来说关键词using
部分展开。例如
// 例如 using std::cout
namespace Er
{int a 1;int b 2;
}
using Er::b; //部分展开的方法全部展开
// 例如 using namespace std;
namespace Er
{int a 1;int b 2;
}
using namespace Er;对于展开的变量或者命名空间来说就相当于暴露在了全局域中这样还是存在命名冲突的风险的所以建议在项目中对于标准库的命名空间尽量展开几个常用的函数或者对象。
2.2.2 使用细节
同一项目下不同文件的同名命名空间是合并的。
namespace Er
{int a 10;
}namespace Er
{int b 5;
}
int main()
{cout Er::a endl; //在源文件2中可以访问到源文件1的Er命名空间变量a
}上面的例子说明我们仍然有可能在我们不之情的情况下在同一项目源文件同名命名空间定义了相同的变量这个时候第二细节排上用场了。
同一源文件的命名空间合并通常是发生在编译阶段。同时需要注意在使用分文件的命名空间的时候通常需要注意使用头文件来声明命名空间。以确保各个源文件中对命名空间的使用是一致的。如果不使用头文件还记得我们上面讲的吗编译阶段每个源文件是各自编译各自的所以如果不采用头文件我们就会发现编译器找不到合适的命名空间内的变量。而上面的例子中是在同一源文件下的操作。如果还是需要分源文件不使用头文件就可以使用extern声明。
命名空间可以嵌套。
举个例子
namespace Cc
{int a 1;namespace Kk{int b 0;int a 2;}
}int main()
{printf(Cc中的 a %d\n, Cc3::a);printf(Cc中的Kk中的 a %d\n, Cc3::Kk::a);printf(Cc中的Kk中的 b %d\n, Cc3::Kk::b);return 0;
}
上面的示例也给出了如何去合理的访问嵌套命名空间的方法。 对比C语言和C解决命名空间的方法我们看到C采用命名空间的方式是比C语言好上太多了避免了C语言的诸多问题极大的提高了程序员编写代码的灵活性。
至此对于命名冲突发生的原因、解决办法已经谈论的差不多了。如果有问题欢迎指出作者接受大家的批评教育。
本文章有参考《C陷阱与缺陷》