前言:C++语法知识繁杂,要考虑的细节很多,要想学好C++一上来就啃书并不是一个很好的方法,书本的内容一般是比较严谨的,但对于初学者来说,很多概念无法理解,上来就可能被当头一棒。因此建议在学习C++之前学好C语言,再听听入门课程,C++有很多的语法概念是对C语言的一种补充,学习过C语言能更好的理解为什么要这样设计,笔者也是初学者,写的这类文章仅是用于笔记总结及对一些概念进行分析探讨,方便以后回忆,因知识有限,错误难以避免,欢迎大佬阅读指教
命名空间
命名空间的定义
阅读下面两段代码,分别用 C++ 和 C语言 打印 “hello C++” 看看其中的区别
#include <iostream> using namespace std; int main() { cout << "hello C++" << endl; return 0; } #include <stdio.h> int main() { printf("hello C++"); return 0; }
我们在看别人写的C++代码时似乎总能看到 using namespace std 这个语句,C++的打印输出函数是 cout ,这可以理解,没有问题,那么为什么要加上using namespace std,这就与命名空间有关了,这个问题,我们等会解决,看下面一段代码
#include <stdio.h> int num = 20; int main() { int num = 10; printf(" %d ", num); printf(" %d ", num); return 0; }
全局变量num 和局部变量num 重名了,按照C/C++编译器的规定,先从局部开始查起,如果局部有该变量的定义,那就用局部的,如果局部找不到该变量的定义,那么就去全局里找,所以两次打印的结果都是10,可我想把全局的 num 和局部的 num 都打印出来呢?C语言是不支持这种形式的,要想把全局的 num 和局部的 num都打印出来,C语言必须有一个变量要改名,而C++可以做到,C++如何做到呢?再看下面一段代码
#include<iostream> using namespace std; namespace test { int num = 20; } int main() { int num = 10; cout << num << endl; cout << test::num << endl; return 0; }
首先忽视using namespace std 这行代码,看看namespace test,namespace就是命名空间的关键字,namespace test就是定义一个名为test的空间域,空间域就相当于一堵墙,把 int num = 20 给围了起来,围起来之后,编译器是不会去域空间里查看的,这样就不会与局部里的num造成命名冲突
那我要使用命名空间里的 int num = 20 该怎么办呢?这个时候就要用域作用限定符 “ :: ",即test :: num ,告诉编译器,去 test 这个空间域里找 num,这个时候打印的就是test 里的 num,需要注意的是 int num = 20 虽然在test空间域里,但num仍然是存放到全局静态区里的,test仅仅相当于一堵围墙,不会影响其到内部的定义和声明,可见命名空间能够起到保护的作用,解决了重名变量之间冲突
为了防止我们个人的变量或函数命名与官方库里的命名产生冲突,C++官方把标准库函数放到了 std 这个空间域里,也就是说我们常用的输出函数cout,换行 endl 都是定义到std这个空间域里的,要想使用cout,endl需要域作用符指定后才能使用,如std :: cout,std :: endl
不加std :: ,编译器压根就不认识cout 和 endl
加了 std :: ,就能够正常编译了,问题又来了,每一次使用 cout 和 endl 都要用域限定符 :: 来指定,这样也太麻烦了,C++官方便允许了 using namespace std; 这种写法,什么意思呢?
cout 不是被 std 这堵墙给围住了嘛,using namespace std就相当于把std这堵墙凿个大洞,里面的定义都暴露到全局域中,这个时候编译器再进行编译的时候,就能够在全局区域里找到cout ,就不需要我们再用 std :: 来指定,当然虽破了个洞,但毕竟还是在里面,因此用 std :: 还是能够找到 cout 的
using namespace std 这样虽然能帮我们偷偷懒,但会导致一个严重的问题,你cout 暴露到全局域里了,假如我在全局域里定义了一个自己的 cout 函数,这不就导致命名冲突了嘛,所以我们自己练习时可以偷懒用,在不明确的环境里慎用
可能你会说,我只是常用cout 而已,并不想把整个std域里的内容都暴露出来,贴心的C++还提供了using std :: cout; 这种写法 ,这意味着只将cout这一个内容暴露到全局域中,std内的其他内容仍处于std的域保护之中
命名空间扩展
上述代码你可能会感到奇怪,int num = 20; 没有受到域的保护,为什么不是只打印局部里的num呢?反而二者都可以被打印出来,第二个打印函数里多了 :: ,:: 的左边如果是空的话,那就默认为全局域,我们之前说过C/C++编译器的规定,查找变量规则是先从局部开始查起,如果局部有该变量的定义,那就用局部的,如果局部找不到该变量的定义,那么就去全局里找
而这里局部里显然是有num的定义的,但在打印第二个num时加上了 :: ,就是告诉编译器,你去全局域里去找num,如果你不加上域限定符 :: ,那么编译器就不会去全局域里找,两次打印的结果就都是10
看上面这段代码, 我分别在 " C++test.h ","C++test.cpp"这两个文件里同时定义了命名空间test ,发现在测试文件里竟然能够运行,得到正确的结果3,这就是命名空间的另一个规则,
在同一项目工程下,不同文件内的相同域名会被合并成一个空间域,也就是 " C++test.h ","C++test.cpp"这两个文件里的同名空间域 test 被合并成一个空间域
缺省函数
缺省函数的定义
缺省函数就是在定义函数的时候,可以给某个参数赋予一个预定值,如果在调用这个函数时,没有给这个参数传值过去,那么这个缺省参数就会使用这个预定值,什么意思呢?
上图中的程序,我想把x,y的值加上一起,然后乘以Z返回,但我并没有给Z传参数,最终得到结果30
细节是我在给Z定义的时候,给Z赋值为10,意思就是在调用该函数时,可以不用给Z传参,如果给Z传参了,那么Z的值就是你传过去的值,如果不给Z传参,那么Z就会使用你在定义时赋予的预定值,此时的Z就是缺省参数,而函数Add就是缺省函数
定义一个函数时,可以把它的所有参数都设为缺省参数,下图把上面的程序改了改,三个参数都设为了缺省值,在调用时,我都不赋值,得到结果3
缺省参数需要注意的
在定义缺省函数时,需要注意的是缺省参数必须依次从右向左定义
int Add(int x = 1, int y , int Z = 1) //不可以,没有依次从右向左 int Add(int x , int y = 1 , int Z = 1) //可以 int Add(int x = 1, int y = 1, int Z) //不可以,没有依次从右向左 int Add(int x = 1, int y = 1, int Z = 1) //可以
给缺省函数传参时,必须从左往右依次传参,不能跳过
缺省函数:int Add(int x = 1, int y = 1, int Z = 1) 调用情况1:Add(1, 2, 3) //可以 调用情况2:Add(1, 2) //可以 调用情况3:Add(1) //可以 调用情况4:Add(1, , 3) //不可以,没有依次传参,中间跳过了一个 调用情况5:Add( , , 3) //不可以,没有依次传参
在定义缺省函数时,缺省参数不能在函数的声明和定义中同时出现
什么意思呢?看下图
在定义中出现还是在声明中出现没有具体的规定,一般都出现在函数的声明中
函数重载
函数重载的定义
什么是函数重载呢?我们在编写项目程序时,如果引用其他的文件里的函数,可能会出现我们自己定义的函数名与其他文件里的函数名重名了,如果你的函数被调用的次数很多,改函数名很麻烦,而其他文件里的函数擅改函数名可能导致莫名奇妙的问题,这种情况就很让人头疼,而C++给我们提供了函数重载来解决这种问题
C++允许两个或多个函数重名,重名可以,但函数的参数必须不同,参数不同指的是参数的个数,参数的类型,参数的顺序(顺序指的是类型顺序)不同,而这两个或多个参数不同的重名函数就构成了函数重载
上图三个test函数就构成了函数重载, 三个test函数的函数名相同,但是其参数分别是int char double 不同的参数类型,是可以构成函数重载的
能否构成函数重载的情况列举
篇幅原因,这里就不写函数的定义了,只展示声明 int test(int x, int y) int test(int x, int y, int z) 上面两个函数是能够构成函数重载的,因为参数的个数不同 int test(int x, int y) char test(char x, char y) 上面两个函数也是能构成函数重载的,因为参数的类型不同 int test(int x, char y) int test(char x, int y) 上面两个函数同样是能构成函数重载的,因为参数的类型顺序不同 int test(int x, int y) int test(int y, int x) 上面两个函数是不能够构成函数重载的,因为这不是类型的顺序不同,而是变量名的顺序不同
函数重载的应用及底层实现
了解过函数重载,大家就能够理解cout函数是如何实现自动识别变量类型的了
cout 本质上是由很多重名的cout函数重载构成的,打印时输入的参数值会传到与它的类型对应的那个cout函数上,从而打印出相应类型的结果
函数重载的底层实现
函数重载是怎么实现的呢?为什么你输入不同类型的值,就能够找到对应的函数呢?在底层是如何区分函数名相同的重载函数的呢?我们一一探讨,这里只是进行简单的探讨,因为这个过程很复杂,展开讲就很麻烦了
编译器会在编译的阶段将出现的各种符号进行解析和汇总,包括变量名,函数名等等,形成一张符号表,便于后续程序调用函数。那想看底层如何区分重载函数的,那就要看看重载的函数在这张符号表上是以什么样的形式记录的,因为不同的编译对符号表中的符号命名规则不同,我们以linux中的g++为参考
假设我们的程序中调用了以下两个函数
int test(int x, int y) double test(double x, doubley)
那么在编译之后,在符号表中,两个函数的符号名分别为
<_Z4testii> <_Z4testdd>
可见,两个重载函数的符号名是函数名与参数类型缩写的合并,这也就对应了前面函数重载的规则
可能会有同学有这样的疑问:如果两个函数的函数名相同,参数也全都相同,但是返回值的类型不同,这样能不能构成函数重载呢?答案是不能的,你可能会说如果在符号表中把返回值的类型再加上不就能够识别这是两个不同的函数了嘛
确实,这样编译器能够识别出这是两个函数,问题的关键是编译器不知道该返回哪个函数,你在调用函数的时候只传过去了参数,你没有说要返回哪种类型,这就造成了二义性,所以不能以返回值的类型来设计重载函数