【c++入门系列】:命名空间以及函数重载以及缺省参数详解

🔥 本文专栏:c++

🌸作者主页:努力努力再努力wz



💪 今日博客励志语录你所做的事情,也许暂时看不到成果,但不要灰心或焦虑,你不是没有成长,而是在扎根。
那么本篇文章是博主c++系列的第一期文章,那么在讲解知识之前,我先来让读者认识一下我以及我以后的c++系列会是一个什么样的方式来呈现给各位读者以及博主我个人的一个博客风格,那么我先来说一下我个人的一个文章风格吧,我对我自己的博客的风格的评价就是详细二字,因为我觉得大家第一眼看到我的博客跟其他人的博客相对比起来就是一个字多,大概我的每篇博客有个七到八千字吧,因为本人确实希望通过一篇文章尽可能记录该知识点的相关所有的细节并且每一篇博客我希望它能够展现出我的一个思考以及理解的一个过程,而对于c++系列来说,由于c++是一门编程语言,那么学习语言的话,肯定核心就是学习c++的相关的语法,但是c++是接近底层的一门编程语言,所以对于我的c++系列的博客来说,那么我的博客不仅要讲解c++的这些语法是怎么用,还要讲解它为什么能这么用,也就是讲解一些底层相关的内容,也就是c++怎么做到,怎么支持这些语法的,对于一个c/c++方向的程序员来说,大部分人想必都不会去造轮子而是去用轮子,但是至少应该知道这轮子的构造以及为什么能够这样用,这是一个c/c++的程序员的一个基本的素养,也就是对底层的探索一个兴趣

那么刚才的所有内容就算是我对我的c++系列的开门大吉吧,那么欢迎各位读者成为我c++系列的第一篇文章的见证者,那么废话不多说,进入我们正文的学习
那么c++这门编程语言是在c语言之后诞生的,那么c++这门语言诞生的一个背景我们可以简单理解为就是为了解决c语言的某些方面的不足,并且从中还添加了自己的一些新的特性,其中最为重要的便是面向对象,最后逐渐便可以独立形成一门新的语言,所以我们学习c++语言的前面大部分就是学习c++在c语言基础上做了什么改善,由于c++是在c语言的基础上做了改善,所以c++本身也是可以兼容c语言的,也就意味着c语言的各种语法以及各种库函数和关键字,那么c++是能够支持,因为c++的编译器本身就保存有c语言的库函数所在的库文件的路径以及本身c++的各种库文件的路径,那么能够进行链接c语言的库函数形成可执行文件,那么我们本文所讲的命名空间便是c++在C语言上所做出的一个改善

命名空间

那么在说命名空间怎么用怎么定义之前,那么我们得先来知道为什么会出现命名空间,也就是命名空间诞生的意义是什么

为什么要有命名空间

那么为什么要有命名空间的话,首先我们就得对一个可执行文件的形成有一个基本的认识,那么平常我们自己写c或者c++代码在VS上或者Dev c++上,那么我们都是编写文件,然后点击生成就能形成一个可执行文件呢,但是你究竟是否清楚的了解从源文件到最终的可执行文件的一个具体的流程呢?如果你对此过程压根就不了解或者说感到有些陌生,那么没有关系,如果你特别清楚,那么可以跳过这部分内容,那么我们来进行一个简单的回顾:

那么形成可执行文件,首先你手上就得持有三种类型的文件,分别是保存你调用函数的声明所在的头文件以及调用函数定义所在的源文件以及主函数所在的源文件,那么有了这三种文件之后,那么他们首先就会经历编译以及链接这两个大的过程,那么其中编译又可以划分成四个环节,分别是预处理以及语法语义检查以及编译和汇编,那么我们先来回顾一下编译的这四个阶段

那么第一步便是预处理阶段,那么预处理阶段的工作其中就包括头文件展开,那么我们的源文件中会有#include XXX.h语句,那么此时在预处理阶段的话,那么该语句所引用的头文件会被编译器逐字逐句的给复制拷贝到引用该头文件中的源文件当中替换该#include语句,那么相当于此时经过预处理阶段之后,源文件中就有了一份函数的声明,那么这就是头文件展开,其中预处理阶段还会进行宏替换以及消去注释,那么此时经过预处理阶段,对应的源文件会此时生成一份对应的后缀名为.i的临时文件,而头文件则不再参与后序的所有过程,因为它的内容被拷贝到引用它的源文件中,那么此时生成的每一个.i文件的内容还是c/c++代码

接着预处理阶段结束后,便进行语法以及语义检查,那么这个阶段的工作,我们就可以简单理解为编译器会检查我们编写的c++代码是否有语法错误,知道这点便足矣,那么至于这个阶段会构建一个语法树,那么这些都是编译原理的知识,那么本文便不再赘述,读者感兴趣下来可以自行了解

而接着就是编译阶段,那么编译阶段的核心工作就是将.i文件中的c/c++代码给转换为汇编码,然后此时还会为每一个文件生成一个对应的符号表,那么这个符号表要记住,我这里先埋一个伏笔,而所谓的符号表就是记录各个文件中的全局变量以及函数的声明以及其对应的定义所在的地址,那么这个地址是逻辑上的地址,也就是在该文件模块中的偏移量而不是物理内存上的地址,因为此时还在编译阶段,压根就还没有形成可执行程序,更不存在加载到内存中运行,所以不存在分配物理内存地址,那么此时每一个文件中可能有函数的声明但没有函数的定义,那么此时编译器不会进行报错处理,因为它知道该符号表中对应的符号没有相应的定义,不代表它没有,而是有可能它的定义存在其他文件当中,那么它会在链接阶段进行一个完善,那么此时每一个.i文件经过编译阶段会生成一个后缀名为.s的文件

那么汇编阶段就是将之前的经过编译阶段的所有的.s文件的内容全部转换为机器码也就是二进制码,因为机器只能识别二进制,那么最终经过汇编阶段后,每一个.s文件会转换为后缀名为.o的中间临时文件


那么最终则是链接阶段,那么链接阶段的内容,之前编译阶段每一个文件都有自己对应的一个符号表,那么在链接阶段,会整合每一个文件的符号表,那么生成一个全局的符号表,那么之前编译阶段中有的文件的符号表中的某个符号只有声明但没有对应的定义,那么此时链接器就会在其他的文件的符号表寻找相应的定义,那么最终整合到全局的符号表中,那么链接阶段之后,最终就会生成一个可执行文件


我们知道符号表是记录每一个文件的全局变量以及函数的声明以及对应的定义,那么在链接阶段,那么此时链接器会整合各个文件的符号表,那么如果说出现了这样的场景,那么假设你现在要定义一个int类型的变量,变量名为a,然后你在a.c源文件中对该变量进行了一个定义,然后你也在b.c文件中也对变量进行了一个定义,那么此时你将包含a.c以及b.c的源文件形成一个可执行文件,那么你会发现此时编译器就会报一个重定义错误,为什么会报这个错误呢,因为我们知道在a.c中对应一个符号表,那么它记录了该符号也就是该变量的声明以及对应的的定义地址假设为0x112233,而同理b.c中也有一个符号表,也记录了该变量的声明以及对应的地址,假设地址为0x133234,那么此时在链接阶段,要形成一个全局的符号表,那么此时链接器要给变量a在全局的符号表中确定填入一个唯一的地址,那么此时链接器发现a有两个不同的地址,那么此时链接器就不知道要究竟填入哪一个地址,那么此时就会报该错误,也就是重定义,同理函数也会出现这种情况

所以在全局作用域出现了同名的变量的定义的话,那么就会出现重定义的情况,所以为了解决这种情况,便有了命名空间的出现


命名空间怎么用

那么我们知道c语言以及c++有作用域这个概念,那么所谓的作用域就是指{...}中的代码块的内容就是一个作用域,那么我们的作用域分为局部的以及全局的作用域,那么要理解这个作用域的话,可以理解为作用域就是建立了一圈围墙,将在这个墙内的所有的数据与外界隔离起来,所以不同的域就可以存在同名的变量的定义,但是在同一个域就不能出现同名的定义,那么我们的命名空间的本质其实就是在全局域中搭建一圈围墙,将其变量给围绕起来,那么此时在命名空间里面的变量就不会与外部的全局域的同名变量产生冲突

那么命名空间的定义需要用到namespace关键字,注意后面不加;

cpp 复制代码
namespace wz
{
    int a=10;
    int c=10;
}

那么命名空间还可以嵌套定义,因为命名空间也是一个作用域,那么难免就会出现作用域中出现命名冲突的情况出现:

cpp 复制代码
namespace wz
{
      namespace jwl
      {
          int a=10;
      }
    int a=20;
}

但是我们写代码的时候不建议命名空间的嵌套过深,不容易阅读以及访问命名空间里面的数据都非常的麻烦

那么知道怎么定义命名空间之后,那么我们怎么进行访问命名空间里面的内容呢?

那么我们首先就得清楚我们一个变量访问的顺序,那么假设我们要在一个局部域中打印一个变量的值,那么此时编译器做的就是先在该局部域中搜索是否有变量的定义存在,如果没有在逐层往外扩展到外层的局部域搜索,最终直到全局域,那么访问的顺序就是局部域到全局域,编译器默认不会到命名空间域里面去搜索

如果我要其能够访问到命名空间里面的数据,那么我们有两种方式:

指定访问:

那么指定访问就是通过域作用限定符::来访问,那么域作用限定符前面其实跟的就是访问的类域或者命名空间域,那么后面跟的是该域中的变量,那么假设我们现在有一个名为wz的命名空间域,那么此时我们要打印该命名空间里面的变量a的值,那么我们可以:

cpp 复制代码
cout<<wz::a<<endl;

而其中由于c++支持了命名空间,那么其中对于c++的标准库包括后面的STL来说,那么他们所有函数以及对象的声明以及定义都被封装在了一个名为std的命名空间中。

那么知道了如何指定访问后,我们可以写一段简单的代码来上手实验一下,假设我在wz命名空间中定义了一个int类型的变量a,然后在全局域中也定义了一个int类型的变量a,那么我们访问这两个域中的值

cpp 复制代码
#include<iostream>
namespace wz
{
	int a = 10;
}
int a = 20;
int main()
{
	std::cout << "a=" << a <<std:: endl;
	std::cout << "a=" << wz::a << std::endl;
	return 0;
}

那么其中我们再来定义一个嵌套的命名空间,其中嵌套的命名空间中也定义了一个int类型的a变量,那么我们再来访问这三个a变量:

cpp 复制代码
#include<iostream>
namespace wz
{
    namespace ywl{
        int a=30;
    }
	int a = 10;
}
int a = 20;
int main()
{
	std::cout << "a=" << a <<std:: endl;
	std::cout << "a=" << wz::a << std::endl;
    std::cout << "a=" << wz::ywl::a << std::endl;
	return 0;
}

展开命名空间

那么展开命名空间就是通过using namespace关键字,后面跟上我们要展开的目标的命名空间,那么它就会将该命名空间里面的所以内容给暴露给全局域,那么指定访问就好比在命名空间中所创建出的围墙打开一扇窗户来窥探里面的内容,而using namespace则是将这个围墙直接给拆除了

cpp 复制代码
using namespace wz;

那么我们还是写一个简单的代码来上手实验一下,那么代码的逻辑也很简单,那么我们现在全局域中有一个int类型的a变量的定义,命名空间里也有一个int类型的a变量的定义,那么我们将命名空间给展开:

cpp 复制代码
#include<iostream>
namespace wz
{
	int a = 10;
}
using namespace wz;
int main()
{
	std::cout << "a=" << a << std::endl;
	return 0;
}

那么我们知道我们命名空间还可以嵌套,那么我们对于嵌套的命名空间展开,那么我们如果展开的外层的命名空间的话,那么内层的命名空间是否会被展开呢?那么验证真理的最好的方法还是实验,那么我们可以写一个简单的c++代码来验证一下刚才的猜想:

cpp 复制代码
#include<iostream>
namespace wz
{
	int a = 10;
	namespace ywl
	{
		int a = 30;
	}
}
using namespace wz;
int main()
{
	std::cout << "a=" << a << std::endl;
	return 0;
}

那么如果说我们的猜想是正确的,也就是说我们展开外层的命名空间,那么内层的命名空间一样也会被展开,那么此时我们跑这段代码的时候,那么就会编译出错,因为重定义了,但是如果说我们运行成功,并且打印a的值是10,那么则说明展开嵌套的命名空间的,只是会展开最外层的命名空间,而不会展开内层的空间

那么由结果可知,展开外层的命名空间不会展开内层的命名空间,所以这里就知道,对于嵌套的命名空间的展开,编译器采取的方式是逐层的由外向内的展开,所以要展开内层ywl的命名空间,那么只能是

cpp 复制代码
using namespace wz::ywl

总结一下展开命名空间,那么就意味着拆除围墙,那么拆除围墙其实就等于了没有命名空间,那么就容易造成命名冲突,所以一般不推荐,一般我们还是建议通过域作用限定符来指定访问。

但是如果不展开的话,假设有这样一个场景,比如我们要打印一个语句,那么我们需要调用c++标准库的cout来打印,那么如果我们要指定访问的话,那么每一个cout的前面都要加一个std::来指定访问,那么在这个场景下,如果我们该程序调用某个命名空间的变量或者对象的次数特别的频繁,那么此时就可以指定将命名空间的某个对象或者变量给展开到全局域当中:

那么具体实现还是利用using关键字

cpp 复制代码
using wz::a

那么我们还是写一个简单的c++代码来实验一下:

cpp 复制代码
#include<iostream>
namespace wz
{
	int a = 10;
	namespace ywl
	{
		int b = 30;
	}
}
using  wz::a;
int main()
{
	std::cout << "a=" << a << std::endl;
	return 0;
}

那么这段代码的逻辑就是来将wz命名空间的变量a展开到全局域中,然后打印其值


命名空间如何做到的

那么我们知道了命名空间如何使用,那么我们再来了解一点底层,也就是命名空间如何实现的,那么我们知道命名空间存在的意义就是出现了重定义,符号表中同一个符号对应了多个不同的定义,那么为了解决这种情况,那么c++的符号修饰规则便加入了命名空间,也就是是c++的符号中记录了函数或者变量名以及参数列表,同时还会记录命名空间,那么一旦记录了命名空间,那么符号不同,那么他们会被认为是一个独立的符号而不是同一个符号,那么每一个符号就可以拥有自己的定义,而不会造成同一个符号拥有了多个不同的定义

命名空间的补充以及要注意的点

那么相信有的读者肯定是一个好奇宝宝,那么他们也许就偏要在自己的代码中,定义两个同名的命名空间,那么如果此时我们在代码中定义了两个同名的命名空间,那么会发生什么了,还是一样,验证真理的最好的方法便是实验,那么我们就写一段简单的代码来实验一下,看会发生什么结果:

代码的逻辑也很简单,定义了两个同名的命名空间wz,那么这两个命名空间中都有一个同名的int类型的变量a的定义

cpp 复制代码
#include<iostream>
namespace wz
{
	int a = 10;
}
namespace wz
{
	int a = 20;
}
int main()
{
	std::cout << "a=" <<wz:: a << std::endl;
	return 0;
}

那么跑一下这段代码看看结果:

那么我们发现报了重定义的编译错误,那么先别急,也许有的读者觉得,我尝试在不同的源文件定义同名的命名空间,是不是会有变数,ok,还是来实验,那么我准备了两个源文件a.cpp以及b.cpp分别定义了同名的命名空间,其中还是包含相同的int类型的变量a的定义

cpp 复制代码
//a.c
namespace wz
{
int a=20;
}
cpp 复制代码
//b.cpp
namespace wz
{
    int a=10;
}
cpp 复制代码
//main.cpp
//namespace wz 的声明保存在test.h中
#include<iostream>
#include"test.h"
int main()
{
	std::cout << "a=" <<wz::a << std::endl;
	return 0;
}

那么我们来跑一下代码:

发现出现了链接阶段的重定义的错误

那么通过这两个简单的实验以及结果,那么我们就可以知道了,如果我们在同一个文件或者不同的文件定义了一个同名的命名空间的话,那么此时编译器会将他们合并在一起,所以我们之前的wz命名空间都对同名的变量a定义了两次,由于同名的命名空间会被合并到一起,所以就会出现重定义的情况出现

函数的重载

为什么要有函数的重载

那么我们知道中华民族文化博大精深,其中就体现在语言上,那么我们的语言巧妙就巧妙在它有一词多义,比如我举一个例子,我们经常拿国足和乒乓球拿来做对比,一个是"谁都赢不了",一个是"谁都赢不了",那么这个就是只可意会不可言传的一词多义

那么同理对于我们c语言来说,我们知道函数是完成某个特定功能的模块,那么如果我们想要同一个函数完成不同的功能的话,那么此时c语言就无法做到,而c++就可以支持,那么这就是函数的重载

函数的重载怎么用

那么在了解函数的重载怎么用之前,我们还是见一见函数的重载,那么我定义两个func函数,但是他们是完成不同类型的数据的加法计算,那么此时我们在主函数调用func函数,看看是什么结果

cpp 复制代码
#inclue<iostream>
using namespace std;
int func(int x,int y)
{
    return x+y;
}
double func(double x,double y)
{
    return x+y;
}
int main()
{
     cout<<func(10,20)<<endl;
    cout<<func(11.1,20,5)<<endl;
}

那么见了见什么是函数的重载,那么如何实现函数的重载呢,那么要实现函数的重载,那么需要满足以下条件:

  • 函数名相同
  • 参数列表不同

而参数列表不同,有可以分为这几种情况:

  • 参数个数不同
  • 参数类型不同
  • 参数顺序不同

那么我们调用有重载的函数时,那么编译器会识别传递的参数的类型来确定匹配对应的重载函数,然后加载该重载函数的实例,比如在上文的例子中,我第一次传的是10和20,那么编译器会识别到是传递的两个int类型的参数,那么是与int func(int x,int y)匹配的,所以会调用该重载函数

函数的重载是如何做到的

那么既然我们学习的是c++,知道轮子怎么用,还得了解一点底层,也就是为什么c++能够支持重载而c语言不能支持重载,那么要知道这一点,那么还得跟我们上文所介绍的形成可执行文件的一个流程有关,那么我们知道在编译阶段的时候,每一个文件都会生成各自对应的一份符号表,那么该符号表就是记录了该文件对应的全局函数以及全局变量的声明以及他们对应的定义的地址,那么我们知道函数的重载,其实虽然函数名是一样的,但是实际上我们知道重载的函数实际上是两份不同的函数,因为重载函数的定义是不一样的,就好比在中国出现名字都叫李华的人很正常,但是他们只是名字一样而已,他们的长相以及身高和身份证号是不同的,意味着他们肯定是不同的人,所以为了区分重载函数,c++的编译器就支持符号修饰,也就是c++的编译器在记录符号的时候,那么该符号的组成不仅仅只有函数名还有其参数列表的类型的信息,比如Linux下的一个符号表中的某个符号为例:

所以这就是为什么我们说同名的函数它的参数列表不同,那么它就支持重载,就是这个道理

那么反过来,我们便能知道c语言为什么不能支持函数重载,那么是因为c语言的编译器它不支持符号修饰规则,也就是说c语言的符号表中记录的各个符号,那么它只有函数名而没有添加参数列表的类型的信息,所以当我们想我们上面那样定义两个或者多个所谓的重载函数,那么对于c语言来说,它记录的符号都是一样的,那么最终的结果就是它发现了你对同一个函数有了多个定义,那么在链接阶段就会出现重定义错误,而对于c++来说根据其符号修饰规则,每一个重载函数在符号表中记录的符号是不同的,所以它每一个符号都会对应一个定义,所以不会出现重定义,那么这也就是为什么c++支持函数重载而c语言不支持的原因,那么函数重载在后面c++的多态中还有十分重要的应用,并且c++的符号修饰规则不会添加返回值,所以返回值不影响重载

缺省参数

为什么要有缺省参数

那么我们也许会遇到这样一个场景,比如在数据结构中,我们自己实现一个栈这个数据结构,那么我们就要定义栈本身的结构定义比如用一个struct结构体以及与栈相关操作的函数,比如栈的初始化以及压栈入栈,那么其中对于栈的初始化来说,我们要给栈一个动态开辟一个初始容量的一个数组,那么这个初始容量就有讲究,如果我们的栈中只存放10个数据,但是我们初始化的时候开辟了500大小的空间,那么就会造成空间浪费,但是如果开辟的太小,又要面临扩容,所以我们可以栈的初始化的时候,如果我们知道实际大概需要多少的容量,那么我们就可以传递一个参数过去,但是如果不知道,那么我们定义一个缺省参数来设置一个合适的值

c 复制代码
struct stack
{
     int * ptr;
    int top;
    int maxsize;
}
void stackinit(struct stack* l1,int default=4)
{
     assert(l1);
    l1->ptr=(int*)malloc(default*sizeof(int));
    if(l1==NULL)
    {
        return;
    }
    l1->top=-1;
    l1->maxsize=default;
    return;
}

缺省参数怎么用

那么缺省参数的实现就是我们在函数的参数列表中设置一个特定的值,如果我们没有显示的指定传入该参数,那么编译器就会自动的用缺省参数来传递:

cpp 复制代码
int func(int x,int y=10)
{
     return x+y;
}

这里func的第二个参数被设置为了缺省参数,那么我们传参的时候,如果只传递了第一个参数,那么第二参数会默认设置为10

那么我们还是来编写一个简单的c++代码来实验一下

cpp 复制代码
#include<iostream>
using namespace std;
int func(int x, int y = 10)
{
    return x + y;
}
int main()
{
    cout << func(10) << endl;
    cout << func(10, 30) << endl;
    return 0;
}

缺省参数的相关补充

那么知道了缺省参数怎么用之后,那么我们得注意一些细节,那么就是由于我们实参传递给形参是从左往右传递的,那么也就意味着你缺省参数只能从右往左设置,比如,你此时设置一个这样的缺省参数

cpp 复制代码
int func(int x=10,int y)
{
    return x+y;
}

那么我们来调用该函数如果是这样调用的

cpp 复制代码
cout<<func(10)<<endl;

那么就会报错:

所以这点我们得注意

其次就是我们现在编写代码想必大家都一定养成了习惯,也就是将调用的函数的声明放在头文件中,而函数的定义保存在源文件中,那么如果我们此时声明和定义都定义了缺省参数,那么会出现什么情况呢,那么其实这个都不用我们自己去写代码验证便知道结果,因为如果编译器允许我们声明以及定义都定义缺省参数的话,那么我们万一发癫,声明与定义给的缺省参数不一致,那么这不就是为难编译器吗,那么编译器到底听声明的还是听定义的,所以为了避免这种情况出现,那么肯定是声明或者定义中其中一个定义缺省参数,所以现在究竟是声明给还是定义给呢?

那么我还是写了一段代码来验证

cpp 复制代码
//test.h
#pragma once
int func(int x,int y);
//test.c
int func(int x,int y=10)
{
return x+y;
}
//main.c
#include<iostream>
#include"test.h"
using namespace std;
int main()
{
    cout << func(10) << endl;
    return 0;
}

之所以出现这个结果就是说明了编译器听声明的,由于声明没有缺省值,那么我们调用func的时候,得传递2个参数,所以只能声明给缺省值而定义不能给缺省值,这是缺省参数一个很关键的细节

结语

那么这就是本篇关于命名空间以及函数重载以及缺省参数的详解,那么读者也可以简单的写一下代码来熟悉一下,那么下一篇c++系列的文章会继续更新c++的语法内容,那么我会持续更新,还希望你能够多多关注与支持,如果本篇文章有帮组到你的话,还请你多多三连加关注哦,你的支持,就是我创作的最大的动力!

相关推荐
大土豆的bug记录1 小时前
鸿蒙进行视频上传,使用 request.uploadFile方法
开发语言·前端·华为·arkts·鸿蒙·arkui
追风少年1552 小时前
常见中间件漏洞之一 ----【Tomcat】
java·中间件·tomcat
yang_love10113 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
hhw1991123 小时前
c#知识点补充3
开发语言·c#
Antonio9153 小时前
【Q&A】观察者模式在QT有哪些应用?
开发语言·qt·观察者模式
Pandaconda3 小时前
【后端开发面试题】每日 3 题(二十)
开发语言·分布式·后端·面试·消息队列·熔断·服务限流
郑州吴彦祖7723 小时前
【Java】UDP网络编程:无连接通信到Socket实战
java·网络·udp
mqwguardain3 小时前
python常见反爬思路详解
开发语言·python
spencer_tseng4 小时前
eclipse [jvm memory monitor] SHOW_MEMORY_MONITOR=true
java·jvm·eclipse
鱼樱前端4 小时前
mysql事务、行锁、jdbc事务、数据库连接池
java·后端