【编译器角度】ODR
引言
全称为One Definition Rule
,可以说是单定义原则或者规则
是为c++的语法规则,这个规则指出变量只能有一次定义。
正文
编译过程
回顾编译的四大过程。预处理,编译,汇编,链接。
预处理 就是替换所有的宏定义,展开include的头文件,去掉注释和一些无关的。[由.c/.cpp 到 .i]
编译 就是将高级语言的代码转换成机器语言的过程,主要进行语法之类的分析,优化成汇编。 [由.i 到 .s]
汇编 就是将源文件翻译成二进制,Windows会转换成.obj文件,类unix的为.o文件。 [.o文件时机器码所以纯文本无法正常显示]
链接 就是将.o文件和库文件绑定到一起,最终形成所谓的可执行程序。
这里的编译器用的visual studio了。
抽象上,.c/.cpp + include<.h> 合并后称为转换单元
然后转换成.obj之类的,因为是windows平台。
最后链接成可执行程序.exe
当一切顺利时,自然不会有问题。语法上的编译器在编译器之前就能给出错误和警告。
1 |
|
主函数调用了一个函数,这个函数在编译的时候会先取一个别名,然后再去替换内存地址。如果不存在就会看到一个lnk类型的报错:
1 |
|
可以看到hh函数其实没有定义过,也未在其他文件有过声明,我们直接调用就会
也就是link链接的时候发生的错误。
如果这个函数没有被调用其实就不会报错了。
未定义行为
c++标准未作规定的行为被称为未定义行为,未定义行为的结果是不确定的,具体表现在不同编译器之间。
- c = 2*a++ + ++a*6
- int x = -25602; x = x >> 2;
拿1来说,a++是规定好的语法,x + a++的话是规定好了,先算x+a,再让a++; 这种简单的复合运算通过提前规定好而规避错误。但是人总是闲的蛋疼,就会整出一些无理由的题,这些题目不能百分之百的给你一个完美的解。
而例1的写法,谁能知道先算a++还是++a呢,如果++a先被计算了,那么前面的值肯定偏大了。抛开被优化了之后,有些可能交给寄存器处理了,所以意义不同了。
例2 纠结之处在于 负数有符号位,补0和1的时候结果大不相同,所以位运算尽量交给正数。
定义
那么ODR的体现就在于程序中定义的每个对象都有自己的规则。
在c++20中 任何变量、函数、类、枚举、模板在每个转换单元中都只允许有一个定义;
在c++17中非inline的函数或者变量在程序中有且只有一个定义;
什么叫定义和声明应该不用多做赘述
1 | void test(); |
像这种多个声明但是只有一个定义就没什么事,否则编译器就会给出报错。
1 | static int a = 250; |
以上几个在一个项目里多个源文件重复定义了才不会报错,因为它们都有各自的内存空间。
链接属性
之前看到过报错信息,函数在链接的时候的名字,c语言是直接在函数名前加了一个_,而c++ 因为有重载,所以要把类型参数等信息表现出来。
链接属性:
- 内部链接属性:就是说这个名称只在本转换单元有效
- 外部链接属性:就是说这个名称在其他的转换单元中也有效
- 无链接属性:就是说这个名称只在作用域内访问
这些个点在函数和变量上都能有所表现。
1 | static hhh(){ |
静态的函数,肯定是内部链接属性,他无法被其他文件调用
静态变量或者const常量,也同样是内部连接属性,在其他文件定义相同名称的并不影响编译
1 | int x = 100; |
这种正常的全局函数或者变量就具有外部链接属性,在本质上他其实在编译的时候就加上了extern,作为外部声明。
所以一个const变量,也可以通过extern 被外部所发现。但是还是少用吧。
如果就是希望这个变量或者函数没有外部属性,最常见就是static限定。
但是c++后期标准不推荐使用static,而是推荐一个未命名空间。
当然标准是一代一代继承的,不可能直接否定掉以前的办法。
inline和static
在c++17之后才出现的inline 变量的写法。
先前的static 变量和函数都是没有问题的。
所以要特别注意下,开发的时候规定项目语法标准,免得有争议。
1 | //main.cpp |
1 | //test.cpp |
在这种情况下,inline的a还能不一样吗?
看到a的值都是main.cpp中定义的。
1 | //test.cppp |
1 | //main.cpp |
再看static的情况
说明inline仍然是外部链接属性,static才是内部链接属性
也就是为什么之前c++17提到非inline的才只有一个定义。
避免的方式也有,就是写进头文件,因为一般定义头文件的时候都会有#pragma once
这种防重的,这样的话就不会管其他地方定义的是什么样的,只看头文件里面就好了。
变量已是如此,函数当然亦是如此~
1 | //test.cpp |
1 |
|
能够看到main.cpp的static void Test写法跟test.cpp没啥差别,但是两个函数作用在不同的文件。
也就说明了static的变量或函数都有不同的内存分配。
函数改成inline结果也不用多想,就会按照main.cpp的来了
结语
倒也不是啥大事,一些编译器和语法标准的问题。