引言

全称为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
2
3
4
5
6
7
8
9
10
11
#include<iostream>

void test(){
std::cout << "hhh" << std::endl;
}

int main() {
test();

return 0;
}

主函数调用了一个函数,这个函数在编译的时候会先取一个别名,然后再去替换内存地址。如果不存在就会看到一个lnk类型的报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>

void test(){
std::cout << "hhh" << std::endl;
}

extern void hh();

int main() {
test();
hh();

return 0;
}

可以看到hh函数其实没有定义过,也未在其他文件有过声明,我们直接调用就会

也就是link链接的时候发生的错误。
如果这个函数没有被调用其实就不会报错了。


未定义行为

c++标准未作规定的行为被称为未定义行为,未定义行为的结果是不确定的,具体表现在不同编译器之间。

  1. c = 2*a++ + ++a*6
  2. 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
2
3
4
5
6
7
void test();
void test();
void test();
void test();
void test(){
std::cout << "hello" << std::endl;
}

像这种多个声明但是只有一个定义就没什么事,否则编译器就会给出报错。

1
2
3
static int a = 250;
const int a = 250;
inline int a = 250; //c++17

以上几个在一个项目里多个源文件重复定义了才不会报错,因为它们都有各自的内存空间。


链接属性

之前看到过报错信息,函数在链接的时候的名字,c语言是直接在函数名前加了一个_,而c++ 因为有重载,所以要把类型参数等信息表现出来。

链接属性:

  1. 内部链接属性:就是说这个名称只在本转换单元有效
  2. 外部链接属性:就是说这个名称在其他的转换单元中也有效
  3. 无链接属性:就是说这个名称只在作用域内访问

这些个点在函数和变量上都能有所表现。

1
2
3
4
5
6
static hhh(){

}
const int h = 100;
static int a = 200;
int main(){...}

静态的函数,肯定是内部链接属性,他无法被其他文件调用
静态变量或者const常量,也同样是内部连接属性,在其他文件定义相同名称的并不影响编译

1
2
3
4
5
int x = 100;
hhh(){

}
int main(){...}

这种正常的全局函数或者变量就具有外部链接属性,在本质上他其实在编译的时候就加上了extern,作为外部声明。
所以一个const变量,也可以通过extern 被外部所发现。但是还是少用吧。

如果就是希望这个变量或者函数没有外部属性,最常见就是static限定。
但是c++后期标准不推荐使用static,而是推荐一个未命名空间。
当然标准是一代一代继承的,不可能直接否定掉以前的办法。


inline和static

在c++17之后才出现的inline 变量的写法。
先前的static 变量和函数都是没有问题的。
所以要特别注意下,开发的时候规定项目语法标准,免得有争议。

1
2
3
4
5
6
7
8
9
//main.cpp
#include<iostream>

inline int a = 350;
void test();
int main(){
std::cout << a << "\n";
test();
}
1
2
3
4
5
6
7
//test.cpp
#include<iostream>

inline int a = 1000;
void test(){
std::cout << a << "\n";
}

在这种情况下,inline的a还能不一样吗?

看到a的值都是main.cpp中定义的。

1
2
3
4
5
6
7
8
//test.cppp
#include<iostream>

inline int a = 1000;
static int b = 2000;
void test(){
std::cout << a << " " << b << "\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//main.cpp
#include<iostream>

inline int a = 350;
static int b = 250;
void test();

int main() {
std::cout << a << " " << b << "\n";
test();

return 0;
}

再看static的情况

说明inline仍然是外部链接属性,static才是内部链接属性
也就是为什么之前c++17提到非inline的才只有一个定义。

避免的方式也有,就是写进头文件,因为一般定义头文件的时候都会有#pragma once这种防重的,这样的话就不会管其他地方定义的是什么样的,只看头文件里面就好了。

变量已是如此,函数当然亦是如此~

1
2
3
4
5
6
7
8
9
10
11
//test.cpp
#include<iostream>

inline int a = 1000;
static int b = 2000;
static void Test(){
std::cout << a << " " << b << "\n";
}
void testp(){
Test();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>

inline int a = 350;
static int b = 250;
void testp();
static void Test(){
std::cout << a << " " << b << "\n";
}

int main() {
std::cout << a << " " << b << "\n";
testp();
Test();

return 0;
}

能够看到main.cpp的static void Test写法跟test.cpp没啥差别,但是两个函数作用在不同的文件。
也就说明了static的变量或函数都有不同的内存分配。

函数改成inline结果也不用多想,就会按照main.cpp的来了


结语

倒也不是啥大事,一些编译器和语法标准的问题。