当前位置: 代码迷 >> 综合 >> C++ day14 内存模型(二) 链接性
  详细解决方案

C++ day14 内存模型(二) 链接性

热度:99   发布时间:2024-02-04 20:04:24.0

文章目录

  • 链接性(linkage,决定了信息(如名称)如何在翻译单元间共享)
    • 外部变量(即全局变量)
    • 单定义规则(每个变量只能有一次定义, 定义声明,引用声明)
      • 内联函数不满足ODR
      • 示例 只有内部链接变量可以被定义在头文件中(::是作用域解析运算符,表示使用变量的全局版本)
      • 不要迷恋全局变量,虽不用传参,但易于访问的代价很大
      • 示例 有内部链接的静态变量对全局变量的隐藏
    • 静态局部变量在编译时静态初始化,而非局部变量的运行时动态初始化
      • 利用静态局部变量做点有趣的统计
    • 终于明白了volatile限定符
    • 使const失能的存储类别说明符:mutable
    • const数据和内联函数的链接属性(特殊)
      • 示例 C++的const全局变量的链接性是内部的,C的仍然是外部链接
    • 函数的链接默认是外部的,即可在文件间共享,但可以设置为内部的
      • C++ 调用函数时怎么查找函数(找的是经过名称修饰后的符号)
    • 语言的链接性(和名称修饰有关)
      • 示例 C和C++的 函数名不可与变量名相同(靠链接性区分不开)
      • 示例 自己写库函数的原型,并用extern说明使用哪个语言的链接方式查找函数

链接性(linkage,决定了信息(如名称)如何在翻译单元间共享)

名称空间提供了共享数据的另一种方法哦,后面说。所以C++共享数据有两条路可选,C只有一条。

链接性分为3种,外部external和内部internal,和无链接。

链接性为外部,可在翻译单元间共享。

链接性为内部,可在翻译单元内共享。

在这里插入图片描述

外部变量(即全局变量)

链接为外部一定有静态存储期。

可以在main函数前面或者头文件中定义,反正在所有函数外面定义就行。

外部变量即全局变量。

单定义规则(每个变量只能有一次定义, 定义声明,引用声明)

ODR, one definition rule
因此C和C++提供两种声明方式。

定义声明会分配内存。只能在一个文件中包含变量的定义。其他文件要用的话,就用引用声明的方式声明,并使用他的引用。

引用声明当然就只是引用已经用定义声明声明的变量啦。原来引用声明中的引用二字真是引用,我之前以为只是名字呢,原来是真的使用了引用这种方式。
引用声明需要使用关键字extern,并且不能初始化,否则编译器就会把这个变量声明当做是定义,会分配空间。

下图就违背了ODR
在这里插入图片描述

内联函数不满足ODR

因为内联函数的原理是用代码本身替代函数调用,所以如果把内联函数写在头文件里,那么每一个包含了该头文件的文件都有这个函数的定义。即整个程序中有多个该函数的定义。

但是这些多个定义是完全相同的哈

示例 只有内部链接变量可以被定义在头文件中(::是作用域解析运算符,表示使用变量的全局版本)

换句话说,不可以在头文件中定义变量,除非加const或者static!

除了const全局变量和内部链接静态变量以外,任何变量都不能定义在头文件,毕竟头文件里又没函数定义更没有块,所以只要定义变量那就是全局变量。

为什么不允许呢?
如果允许在头文件定义变量,则变量是全局变量,即有外部连接性,如果文件A包含了这个头文件,于是头文件的代码被文本替换到文件A包含头文件的地方,相当于在文件A定义的这个全局变量,于是和文件A在一个程序的所有文件都能访问到这个全局变量。这·····大大地破坏了信息隐藏/数据隐藏的艺术之美,是很不安全的操作。所以C++禁止在头文件定义变量。

const变量可以定义在头文件是因为:const变量的链接性是内部的,也就是不会破坏数据隐藏的艺术之美,程序很安全。后面会具体说

说白了,只要变量的连接性是内部的,就可以定义在头文件中。我对这个猜测做了验证,发现果然可以在头文件定义static变量,毕竟头文件没有函数没有块,static变量就是内部链接的静态变量,可以成功

例如

//coordin.h
#ifndef COORDIN_H_
#define COORDIN_H_
const int SIZE = 20;//const全局变量
static int a = 1;//内部链接静态变量
//int b = 1;//报错,外部变量
#endif // COORDIN_H_
//main.cpp
#include <iostream>
#include "coordin.h"int main()
{std::cout << "const global variable SIZE = " << SIZE << '\n';std::cout << "static variable with internal linkage a = " << a << '\n';return 0;
}

在这里插入图片描述

const global variable SIZE = 20
static variable with internal linkage a = 1

我真的不能放过每一个示例,因为不能太相信自己,就算看似再简单的东西,还是有可能会犯错,毕竟没掌握扎实,通过犯错来巩固记忆

虽然这个示例本来是想岩石全局变量被隐藏,以及全局变量在别的文件也可见,但是这些都很简单,我错在,我的脑子竟然直觉觉得应该要把全局变量放在头文件,因为要共享,不知道脑回路当时怎么连的,写出这个程序后一直报错
在这里插入图片描述

主程序

#include <iostream>
#include "coordin.h"
double warming = 0.3;//外部变量/全局变量的定义声明int main()
{std::cout << "global warming is " << warming << '\n';update(0.1);//更改全局变量的值std::cout << "Now global warming is " << warming << '\n';local();//同名局部变量隐藏全局变量return 0;
}

头文件

#ifndef COORDIN_H_
#define COORDIN_H_void update(double);
void local();
#endif // COORDIN_H_

函数文件

#include <iostream>
#include "coordin.h"
extern double warming;//引用声明void update(double x)
{warming += x;
}void local()
{double warming = 1.2;//隐藏全局变量warmingstd::cout << "local warming is " << warming << '\n';std::cout << "But global warming is " << ::warming << '\n';//::是作用域解析运算符,表示使用变量的全局版本
}

结果

global warming is 0.3
Now global warming is 0.4
local warming is 1.2
But global warming is 0.4

不要迷恋全局变量,虽不用传参,但易于访问的代价很大

全局变量虽然牛逼,但是反而应该少用,因为它没有实现数据隔离,或者说数据隐藏,而编程尤其是OOP非常重视数据隐藏,甚至认为数据隐藏是一种艺术。所以其实还是弱一点的局部变量应用更多,而且应该多用局部变量。而全局变量主要用来定义一些多个文件都要用的常量,单个值或者数组,结构都行,通常还会用const保护起来。(其实吧const常量声明为外部变量,我们已经都用习惯了,经常用,现在明白其中的设计和考虑)

比如

const char * const char months[12] = 
{"January",  "Februry", "March", "April", "May", "June","July", "August", "September", "October", "Novenmber", "December"
};

这个数组保护的特别周全,两道屏障,第1个const保证了数组是常量数组,元素都是常量,即不能通过这个指针来修改数组元素的值;第2个const保证了指针是const常量,即永远指向这个数组的第一个字符串

示例 有内部链接的静态变量对全局变量的隐藏

局部变量,内部链接的静态变量,全局变量,越来越强,强者却会被弱者隐藏。

这个示例,我展示了所有三种隐藏情况:

  • 局部变量 隐藏 全局变量(可以说成是外部链接的静态变量)
  • 局部变量 隐藏 内部链接静态变量
  • 内部链接静态变量 隐藏 全局变量
//coordin.h
#ifndef COORDIN_H_
#define COORDIN_H_
void local();
#endif // COORDIN_H_
//main.cpp
#include <iostream>
#include "coordin.h"
int sue = 3;//外部变量/全局变量的定义声明
int tom = 5;
static int amy = 6;//内部链接的静态变量int main()
{std::cout << "In main():\n&sue is " << &sue << ", sue is " << sue << '\n';std::cout << "&tom is " << &tom << ", tom is " << tom << '\n';std::cout << "&amy is " << &amy << ", amy is " << amy << '\n';local();return 0;
}
//file1.cpp
#include <iostream>
#include "coordin.h"
extern int sue;//引用声明
static int tom = 8;//内部链接静态变量隐藏全局变量
int amy = 12;void local()
{std::cout << "In local():\n&sue is" << &sue << ", sue is " << sue << '\n';std::cout << "&tom is " << &tom << ", tom is " << tom << '\n';std::cout << "&amy is " << &amy << ", amy is " << amy << '\n';int sue = 23;//局部变量隐藏全局变量int tom = 56;//局部变量隐藏内部链接静态变量std::cout << "After defining local variables:\n&sue is " << &sue << ", sue is " << sue << '\n';std::cout << "&tom is " << &tom << ", tom is " << tom << '\n';
}

输出

In main():
&sue is 0x4b8008, sue is 3
&tom is 0x4b800c, tom is 5
&amy is 0x4b8010, amy is 6
In local():
&sue is0x4b8008, sue is 3
&tom is 0x4b8000, tom is 8
&amy is 0x4b8004, amy is 12
After defining local variables:
&sue is 0x6efecc, sue is 23
&tom is 0x6efec8, tom is 56

静态局部变量在编译时静态初始化,而非局部变量的运行时动态初始化

总之只会被初始化一次,调用函数时不会再动态初始化,多次调用函数也不会初始化,其实这一点很有趣,很有用。看下面的示例

利用静态局部变量做点有趣的统计

//coordin.h
#ifndef COORDIN_H_
#define COORDIN_H_
const int SIZE = 20;
void eatline();
void strcount(char st[]);
#endif // COORDIN_H_
//main.cpp
#include <iostream>
#include "coordin.h"int main()
{using std::cin;using std::cout;char ar[SIZE];cout << "Enter a string (empty line to quit:):\n";//cin遇到空白符就罢工while (cin.get(ar, SIZE) && ar[0] != '\n')//如果读取成功就进来{eatline();//读取没读完的字符strcount(ar);cout << "Enter a string (empty line to quit:):\n";}return 0;
}
//file1.cpp
#include <iostream>
#include "coordin.h"void strcount(char st[])
{int count = 0;//局部变量,存储每一个字符串的长度static int total = 0;//静态局部变量,存储输入的所有字符串总长度,程序运行期间只会初始化一次char * str = st;while (*str){++count;++str;}total += count;std::cout << st << " has " << count << " characters.\n";std::cout << "total: " << total << " characters\n";
}void eatline()
{while (std::cin.get() != '\n');
}

输出

Enter a string (empty line to quit:):
ok
ok has 2 characters.
total: 2 characters
Enter a string (empty line to quit:):
It's good. It's good. has 10 characters.
total: 12 characters
Enter a string (empty line to quit:):
I am great!
I am great! has 11 characters.
total: 23 characters
Enter a string (empty line to quit:):Process returned 0 (0x0)   execution time : 41.330 s

当输入字符超过数组长度时,只会读取数组长度-1个字符,剩余的被eatline吃掉了,嘻嘻,就不会留在输入队列影响下一次输入。这种在下一次输入前提前处理掉缓冲区已经做过无数次了

如果用cin.getline(ar, SIZE), 则当输入的字符串超过数组长度,cin就会返回false,程序退出,所以这里只能用get方法

Enter a string (empty line to quit:):
aaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaa has 19 characters.
total: 19 characters

终于明白了volatile限定符

之前说实话,从没真正理解到volatile的用处和含义,只大概背书似的知道和编译器优化有关,于是就感觉很神秘隐晦和玄妙。今天终于揭开了veil

有些变量,其值就算你不用代码写具体的计算或者赋值去直接改变, 它也可能受到硬件或者外来输入的影响被改变,比如存储系统时间的变量,总之你不改变它他也有可能被改变。

而编译器,我们知道,设计编译器,即开发编译器程序,是一个辛苦活,做的事本质上就是把程序员的高级语言代码翻译为机器看得懂的机器代码,那么翻译是要讲究质量的,即编译器开发工作者需要想办法编译出尽量高效快捷的机器码,也就是做一些优化。

举个例子,如果你连续好几句代码都要使用同一个变量,那么编译器为了节省时间提高效率,就不再每次都到内存中去取这个变量的值,而是直接把这个变量的值复制到比内存更快的寄存器中,我们知道,内存比硬盘快100多倍,寄存器又比内存快上一百多倍,所以CPU可以直接在寄存器取值,时间就节省出来了。效率就上去了。

但是,对于刚才所说的那种变量,我们不能让编译器这么优化,因为可能几次使用中,值已经变了。如果还那么优化,就会出错。所以C的设计者弄了一个volatile关键字,告诉编译器,这个变量多变,你不要优化它啦,从而避免了错误。C++沿用。

使const失能的存储类别说明符:mutable

mutable的中文意思:可变的,变异的

const的全名:constant, 常量,即不可改

mutable作用于const限定的结构或者类的成员(注意const限定结构或者类,mutable说明的是他们的成员),则这一个成员的值可以改。比如:

//main.cpp
#include <iostream>
struct data
{char name[20];mutable int count;
};int main()
{const data sue = {"Sue Green", 10};std::cout << sue.name << ' ' << sue.count << '\n';sue.count = 20;char *amy = "Amy Galler";//sue.name = amy;//报错,无法通过编译std::cout << sue.name << ' ' << sue.count << '\n';return 0;
}

可以看到,count被改了

Sue Green 10
Sue Green 20

const数据和内联函数的链接属性(特殊)

正是由于他俩的特殊链接属性,所以才可以放在头文件中。

示例 C++的const全局变量的链接性是内部的,C的仍然是外部链接

全局变量的链接性是外部的,但是const外部变量例外,所以可以放在头文件中,而且包含该头文件的每一个源代码文件都私有了一个该const常量。

我专门写了个C程序来验证,注意要用C编译标准,我用的C11

//main.c
#include <stdio.h>
const int SIZE = 10;//C的const全局变量也是外部链接
void myprint();int main()
{myprint();printf("In main(), SIZE = %d\n", SIZE);return 0;
}
//file1.c
#include <stdio.h>
extern const int SIZE;//一定要做引用声明,不然用不了,编译器会说你没定义变量SIZEvoid myprint()
{printf("In myprint(), SIZE = %d\n", SIZE);
}

可以看到,file1.c中没有定义式声明SIZE,也没有包含定义了SIZE的头文件,所以是main()函数所在的文件的SIZE被共享了,因此C的const全局变量拥有外部链接

In myprint(), SIZE = 10
In main(), SIZE = 10

把上面代码改为C++的,只是改改输入输出啦

//main.cpp
#include <iostream>
const int SIZE = 10;//C的const全局变量也是外部链接
void myprint();int main()
{myprint();std::cout << "In main(), SIZE = " << SIZE << '\n';return 0;
}
//file1.cpp
#include <iostream>
extern const int SIZE;//一定要做引用声明,不然用不了,编译器会说你没定义变量SIZEvoid myprint()
{std::cout << "In myprint(), SIZE = " << SIZE << '\n';
}

报错,即使extern引用声明了的也不行,因为人家只有内部连接
在这里插入图片描述

把代码中的SIZE声明再加一个extern在const前面,就可以强制使得这个const全局变量具有外部链接

extern const int SIZE = 10;

输出

In myprint(), SIZE = 10
In main(), SIZE = 10

当然,证明使用的是同一个变量的最好办法是打印地址,这里就不折腾了,都写好了

函数的链接默认是外部的,即可在文件间共享,但可以设置为内部的

默认是外部的,但你也可以在定义和原型前面加个extern,但是没人这么做,因为没必要呀。就像C的auto一样,明摆着的事情还加一个词干啥。

所以同一个程序的不同文件的函数都是相互可见的。

在定义和原型前面加个static就成了内部链接的函数,也叫静态函数,只能在当前源文件中使用。注意静态函数中的静态并不是说存储持续性,而是说链接,这是static的关键字重载,即具体含义要看上下文。

想起了大二时写Java,每次一上来就是public static void main()

C++ 调用函数时怎么查找函数(找的是经过名称修饰后的符号)

按顺序:先看是不是静态,如果是静态,就只在当前翻译单元找函数的机器码;如果不是静态的,那就在该程序的所有翻译单元找;如果有两个定义,那就报错(不是说重载哈,重载是函数名相同,参数列表不同,返回值类型不管;这里说的是三者全部相同但定义不同);最后才找库。

很好理解,先找范围小的,再找范围大点的,最后才去找最大范围。尽量快嘛
在这里插入图片描述

语言的链接性(和名称修饰有关)

C语言的linkage:(只是做一点点无关紧要的处理)
C语言没有重载特性,所以一个函数名一定只对应一个函数,所以不需要名称修饰,但是C语言编译器仍然会进行一点工作,比如把函数名前面加一个下划线,比如函数名spiff被编译器翻译为_spiff, 于是有函数调用spiff函数时,就去找_spiff;

我猜测(马上被下面这个示例啪啪打脸):编译器之所以这么做,是为了保证函数名不会和某个变量名相同。就算你相同,编译器在内部偷偷对函数名加了下划线,但没有对变量名加,所以他也可以区分的开。

示例 C和C++的 函数名不可与变量名相同(靠链接性区分不开)

难道是因为C会对变量也做一样的处理??不懂

#include <stdio.h>
void myprint();int main()
{int myprint = 1000;printf("In main(), variable myprint = %d\n", myprint);myprint();return 0;
}void myprint()
{int SIZE = 9;printf("In myprint(), SIZE = %d\n", SIZE);
}

C认myprint为一个变量,不认他是函数
在这里插入图片描述
好奇试了试C++,也不允许

//main.cpp
#include <iostream>
const int SIZE = 10;//C的const全局变量也是外部链接
void myprint();int main()
{int myprint = 1000;std::cout << "In main(), variable myprint = " << myprint << '\n';myprint();return 0;
}void myprint()
{std::cout << "In myprint(), SIZE = " << SIZE << '\n';
}

在这里插入图片描述
C++的linkage:
但是C++,Java,python等都有重载大法,于是一个函数名可能对应多个函数,所以编译器必须进行名称修饰,即根据返回值类型和参数列表对函数名“编码”,使得每一个函数都有一个独一无二的名称,这才能保证编译器可以找得到它。

比如把spiff(int) 编码为_spiff_i, 把spiff(double, double)编码为_spiff_d_d,只是比如哈

如果要在C++程序中使用C库的预编译函数,那么就要注意一下查找函数的约定了。毕竟C++和C的链接性不一样,你直接以C++的方式去找,肯定找不到。要用extern关键字显式告诉编译器使用哪一个语言的链接。

示例 自己写库函数的原型,并用extern说明使用哪个语言的链接方式查找函数

对这个示例,我特别自豪和开心,因为语言的链接性,并不算一个特别重要的知识点,但我还是顺着自己的好奇心在这里琢磨了好多好久,敲的代码都是为了印证自己的猜测,很棒很棒的

不功利,纯粹出于喜欢在钻研

//main.cpp
#include <iostream>
extern "C" int strlen(char *);//显式说明使用C链接方式查找函数
//extern int strlen(char *);//默认使用C++链接方式查找函数
//extern "C++" char * strcpy(char *, char *);//显式说明使用C++链接方式查找函数int main()
{char st[10] = "great!";std::cout << strlen(st) << '\n';return 0;
}

正确输出

6

但是如果使用被注释的两个原型,都会报错:
在这里插入图片描述

因为strlen函数本来就是C的库函数,库函数是已经编译好的机器码,所以函数已经用了C的链接,因此在我写的CPP程序里显式告诉编译器用C的链接方式来查找函数就能成功。但是用C++的链接方式,就不行了。

还想写一个用C++链接方式的,那就必须得是C++独有的库函数,即不能是C遗传来的,否则就是用C预编译的了,于是我找到了getline函数,不是cin.getline哈,写了如下代码,我真的好难,写了好久,总算是得到了想要的结果

//main.cpp
#include <iostream>extern "C" int strlen(char *);//显式说明使用C链接方式查找函数,可行
//extern int strlen(char *);//默认使用C++链接方式查找函数,不行
//extern "C++" strlen(char *);//显式说明使用C++链接方式查找函数,不行//extern "C" istream & istream::get(char &c);//不可以
//extern std::istream & getline(std::istream &is, std::string &str);//可以
extern "C++" std::istream & getline(std::istream &is, std::string &str);//可以int main()
{char st[10] = "great!";std::cout << strlen(st) << '\n';std::string st1 = "difficult";std::string & str = st1;std::cout << str << '\n';std::getline(std::cin, str);std::cout << str << '\n';return 0;
}

这个代码的印证C++部分一直没成功,最后才知道,原来是忘记写std::getline(std::cin, str);中的std::

故意不写万能的using namespace std;

就是为了有一天解决这样的bug,之前看到sqrt函数前面也有这个前缀,印象特别深刻,所以今天调代码山穷水尽疑无路时想到了这一点,成功解决问题

6
difficult
I almost cry!!!
I almost cry!!!

当然,如果你直接包含string头文件, 不自己写原型,也可以,这其实才是我们平时用的办法勒

#include <string>
  相关解决方案