• C++打怪升级(一)- 命名空间、缺省形参、重载


    BlogPicture7.jpg

    前言

    从今天开始就来到了有关C++的学习了,让我们稳步前行把!


    引子

    C++从何而来

    我们知道C语言是面向过程的程序化、模块化的语言,适合处理较小规模的程序。对于复杂的问题、规模较大的程序、需要高度的抽象和建模时使用C语言则不是很合适。
    20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言也应运而生。
    1982年,Bjarne Stroustrup(本贾尼)博士在C语言的基础上引入并扩充了面向对象的概念,发明了一
    种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。
    C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点基于对象的程序设计,还可以进行面向对象的程序设计


    C++重要发行版本

    C withclasses类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等
    C++98C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)
    C++11增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库
    C++20自C++11以来最大的发行版引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等

    C++从C而来

    C++语言开始时是为了弥补C语言本身和其在面向对象程序设计时的不足。
    C++语言是兼容C语言的,所以C++的编译器可以兼容编译C语言所写的程序。
    C++语言相比C语言实现了更高层次的封装,是更高级的语言。


    C++关键字一览

    C++相比C语言(32个)引入了更多的(63个)关键字,这一点也可以管中窥豹看出一点C++的复杂。

    asmdoifreturntrycontinue
    autodoubleinlineshorttypedeffor
    booldynamic_castintsignedtypeidpublic
    breakelselongsizeoftypenamethrow
    caseenummutablestaticunionwchar_t
    catchexplicitnamespacestatic_castunsigneddefault
    charexportnewstructusingfriend
    classexternoperatorswitchvirtualregister
    constfalseprivatetemplatevoidtrue
    const_castfloatprotectedthisvolatilewhile
    deletegotoreinterpret_cast

    命名空间

    C语言的缺陷之名字冲突

    先来看看一个有问题的C语言程序:

    #include 
    int a = 0;
    int a = 0;
    int main() {
    
    	printf("%d\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    定义了两个全局同名变量a,编译该程序将出现错误。

    image.png

    这与作用域有关系:
    对于C语言在同一作用域中不能定义同名的多个变量。


    C++的解决之道

    我们虽然不会故意去写上面的那种代码,但是在与他人的协作中出现变量同名现象却也是可能发生的。
    C++为了解决C语言所面临的的类似这样的名字冲突问题,引入了命名空间的概念。

    命名空间定义

    每一个命名空间都是一个新的独立封闭的作用域,是C++对C语言中作用域的扩展。
    命名空间关键字:namespace
    创建一个命名空间:使用namespace关键字,后面跟命名空间的名字,最后用{}包含命名空间的成员。
    namespace tag{ .... }

    #include 
    namespace N {
    	int a = 0;
    
    	int Add(int a, int b) {
    		return a + b;
    	}
    
    	struct ListNode {
    		int data;
    		struct ListNode* next;
    	};
    }
    
    int main() {
    	int a = 1;
    	std::cout << N::a << std::endl;
    	std::cout << N::Add << std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    于是,我们有了解决办法:把定义的全局变量放入不同的命名空间域中

    #include 
    
    namespace N1 {
    	int a = 0;
    }
    namespace N2 {
    	int a = 0;
    }
    
    int main() {
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那么我们应该如何访问到命名空间中的变量、函数、类等我们所定义的成员呢?
    image.png
    直接访问肯定是行不通的,因为命名空间是封闭的,或者说命名空间会影响编译器对标识符的查找规则,导致编译器默认不会到命名空间域中查找。

    编译器查找标识符规则
    默认情况:先在标识符所在作用域向上查找,找不到再去全局作用域向上查找,都找不到就报错;
    指定(特定)查找:需要使用域作用限定符::,直接去指定的作用域去查找标识符,找不到直接报错。

    域作用限定符::

    scope::tag

    两个操作数:左操作数是指定的一个域,右操作数是一个标识符(变量、函数名等)
    使用域作用限定符访问命名空间中的成员**:**

    #include 
    
    namespace N1 {
    	int a = 0;
    }
    namespace N2 {
    	int a = 0;
    }
    
    int main() {
    	printf("%d\n", N1::a);
    	printf("%d\n", N2::a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    于是,多使用命名空间就很好的解决了名字冲突的问题。

    命名空间其他定义方式

    嵌套定义

    即一个命名空间可以包含另一个命名空间或另外几个命名空间。

    //命名空间的嵌套
    #include 
    namespace N1 {
    	int a = 0;
    	int Add(int a, int b) {
    		return a + b;
    	}
    
    	namespace N2 {
    		int a = 0;
    		int Add(int x, int y) {
    			return x + y;
    		}
    	}
    }
    
    int main() {
    	int rand = 1;
    	std::cout << &N1::a << std::endl;
    	std::cout << &N1::N2::a << std::endl;
    	std::cout << N1::Add << std::endl;
    	std::cout << N1::N2::Add << std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    image.png

    同一工程中同名的命名空间可以同时存在

    在编译后同名命名空间的成员将会合并到一个命名空间里。

    #include 
    namespace N1 {
    	int a = 0;
    	int Add(int a, int b) {
    		return a + b;
    	}
    }
    
    namespace N1 {
    	int b = 0;
    	int Sub(int a, int b) {
    		return a - b;
    	}
    }
    
    int main() {
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    注意,合并前的同名命名空间成员不能同名,否则在合并后的同一个命名空间域中就有了同名成员,即重定义,程序将会报错。

    #include 
    namespace N1 {
    	int a = 0;
    	int Add(int a, int b) {
    		return a + b;
    	}
    }
    
    namespace N1 {
    	int a = 0;
    	int Sub(int a, int b) {
    		return a - b;
    	}
    }
    
    int main() {
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image.png


    命名空间的使用

    前面已经介绍了使用域作用限定符访问命名空间中成员的方法,接下来我们来看看另外两种访问命名空间成员的方法吧!
    在介绍之前先来了解一下C++官方库定义的命名空间std

    std - C++标准库的命名空间名

    C++中的头文件定义的所有内容成员(定义与实现)都处在一个命名空间(作用)域std中,用以与用户使用的成员相隔离。
    我们想要使用头文件的成员时不仅需要包含相应的头文件(预处理时该头文件将会在包含位置处全部展开),还需要再进一步去到命名空间std中寻找所需要的成员。


    使用关键字using将命名空间某个成员引入

    又称为命名空间的部分展开;

    • 引入该成员后,就可以直接使用该成员了,不需要再用域作用限定符了(当然用了也不会出错)。
    • 本质是该成员的作用域发生了变化:从命名空间域改变为了引入处的作用域。
    • 优点是使用命名空间成员方便了
    • 缺点是引入处作用域如果有与引入成员相同的标识符会引发程序出编译错误。
    //命名空间 - 部分展开
    #include 
    using std::cout;
    using std::endl;
    
    int main() {
    	cout << "hello!" << endl;
    	std::cout << "hello!" << std::endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    使用关键字using namespace将命名空间名称(所有成员)引入

    又称为命名空间的全部展开

    • 可以直接使用命名空间内所有的成员了
    • 所有成员的作用域发生了变化:从命名空间域改变为了引入处的作用域。
    • 相对于优点来说,缺点更加明显了。命名空间往往有很多成员,我们可能也不知道哪些成员在命名空间定义了,哪些又没有定义,这种情况极易与我们自己程序的标识符(变量、函数、类)等发生名字冲突,C语言面临的问题又显现了出来。
    #include 
    using namespace std;//
    
    int main() {
    	int a;
    	double d;
    	char c;
    	cin >> a >> d >> c;
    	cout <<"int: " << a << " " << "double: " << d << 
    		" " << " char: " << c << " " << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    C++的输入和输出

    C++语言本身并没有输入输出语句,实际上C++的输入输出是通过函数调用实现的,这一点与C语言相似。
    我们先来看一个输入输出的例子:

    #include
    // std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
    using namespace std;
    int main(){
        
        cout<<"Hello world!"<<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. cout(读作see out)和cin(读作see in)分别是ostreamistream类型的对象,使用cout标准输出对象(默认绑定控制台)和cin标准输入对象(默认绑定键盘)时,必须包含< iostream >头文件
      以及按命名空间使用方法使用std
    2. coutcin是全局的流对象endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
    3. <<是流插入运算符(输出运算符),>>是流提取运算符(输入运算符),>>和<<涉及运算符重载。
    4. C++的输入输出可以自动识别变量类型,非常方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。当然可以两种输入输出都使用,C++输入输出某些情况可能不会很方便:如控制浮点数小数点位数、左右对齐等,这时printf、scanf反而会是更好的选择,我们用着顺手才是最好的

    注意:早期标准库将所有功能(定义)在全局域中实现,声明在.h后缀的头文件中使用时只需包含对应
    头文件即可,后来将其实现在
    std命名空间下为了和C头文件区分,也为了正确使用命名空间,
    规定C++头文件不带.h
    旧编译器(如vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用+std的方式 。

    std命名空间的使用惯例:
    日常练习中直接使用using namespace std;完全展开std即可;
    实际开发中指定命名空间中特定成员展开即可,如:using std::cout;using std::endl;


    C++的标准错误

    cerr(读作see error)与 标准错误关联ostream对象
    clog(读作see log)与标准错误关联,报告程序的执行信息ostream对象

    缺省参数

    在了解缺省参数前,我们先来看看一个简单的普通函数其打印一个整数到控制台console

    #include 
    using namespace std;
    
    void function(int val) {
    	cout << val << endl;
    }
    
    int main() {
    	function();//函数需要接受一个int型参数,否则报错
    	function(10);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image.png
    程序报错了,这是C语言的不足之处:无法灵活接收参数,很容易写出僵硬的代码。
    C++对此进行了有效的改进:即缺省参数的引入。


    缺省参数是啥

    缺省参数即函数默认形参参数,在定义或声明函数时,其形参可以直接给出形参合适的缺省(默认)值;在调用含有缺省参数的函数时,调用者就可以选择对缺省参数是否进行实参的传入了,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参 。也就是说,虽然是同一个函数,却出现了不同的调用形式。

    #include 
    
    void function(int val = 0) {
    	printf("%d\n", val);
    }
    
    int main() {
    	function();
    	function(10);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image.png

    缺省参数的运用

    函数定义或声明时部分缺省参数的设置规则:

    形参从右向左可以连续缺省;不从右开始或有跳跃则报错。

    正确举例:
    image.png
    image.png
    错误举例:
    image.png

    函数调用时传实参规则:

    实参可以从左向右连续传入;不从左向右或·有跳跃则出错。

    正确举例:
    image.png
    错误举例:
    image.png

    全缺省参数

    即定义或声明的函数形参都是有缺省值(初始值、默认值)的。

    #include 
    using namespace std;
    
    void Func(int a = 0, int b = 0, int c = 0) {
    	cout << "a " << a << endl;
    	cout << "b " << b << endl;
    	cout << "c " << c << endl;
    }
    
    int main() {
    	Func();
    	Func(100);
    	Func(100, 200);
    	Func(100, 200, 300);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image.png


    部分缺省参数

    即函数定义或声明中部分形参有缺省值(初始值、默认值)

    #include 
    using namespace std;
    
    void Func(int a, int b = 0, int c = 0) {
    	cout << "a " << a << endl;
    	cout << "b " << b << endl;
    	cout << "c " << c << endl;
    }
    
    int main() {
    	Func(100);
    	Func(100, 200);
    	Func(100, 200, 300);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image.png


    含缺省参数的函数注意点

    1. 声明和定义分离时的

    缺省参数不能在函数声明和定义中同时出现 。
    也就是说,想要告诉编译器是缺省函数只需要告诉一次就可以了(声明或定义,而不是声明和定义)。

    一般来说,缺省参数出现在函数声明中即可(也就是头文件中)。
    如果在声明和定义中都出现缺省参数,缺省参数可能会不一致导致二义性。

    1. 缺省值必须是常量(#define、枚举、字面值等)或全局变量。
    2. **C语言不支持,是对C语言的改进。 **
    3. 半缺省参数必须从右往左依次来给出,不能间隔着给

    函数重载

    一上来看到重载也许你会疑问,重载是什么含义?
    重载可以理解为一词多义,这一点我们应该不会陌生。
    我们先来看一个有问题的C语言程序,全局域中有两个同名函数名Add,虽然他们的参数类型不同、返回值也不同:

    #include 
    
    int Add(int a, int b) {
    	return a + b;
    }
    
    double Add(double x, double y) {
    	return x + y;
    }
    
    int main() {
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    image.png

    C语言中并不支持在同一个作用域中出现同名的函数,但是C++在满足一些条件时可以支持。
    函数重载也是C++对C语言的有关函数调用时改进。

    函数重载是什么

    函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似同名函数,这
    些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
    不同的问题。

    //函数重载 - 一词多义
    #include 
    using namespace std;
    
    int add(int a, int b) {
    	return a + b;
    }
    
    double add(double x, double y) {
    	return x + y;
    }
    
    
    int main() {
    	cout << add(10, 20) << endl;
    	cout << add(10.2, 20.3) << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image.png


    函数重载类型

    参数个数不同

    #include 
    using namespace std;
    
    void func() {
    	cout << "func()" << endl;
    }
    
    void func(int a) {
    	cout << "func(int a)" << endl;
    }
    
    int main() {
    	func();
    	func(10);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image.png


    参数类型不同

    #include 
    using namespace std;
    
    void func(int a) {
    	cout << "func(int a)" << endl;
    }
    
    void func(double a) {
    	cout << "func(doublea)" << endl;
    }
    
    int main() {
    
    	func(10);
    	func(3.14);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image.png


    参数顺序不同

    #include 
    using namespace std;
    
    void func(int a, char b) {
    	cout << "func(int a, char b)" << endl;
    }
    
    void func(char a, int b) {
    	cout << "func(char a, int b)" << endl;
    }
    
    int main() {
        
    	func(1, 'a');
    	func('a', 10);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image.png


    名字修饰 - C++支持重载的原理

    C语言引入

    先在我们知道C语言不支持重载,而C++支持,那么为什呢?C语言为什么不支持?C++又做了哪些改进从而支持了重载呢?
    这一切的一切需要从程序运行前的编译和链接说起。
    我们知道一个C/C++程序从源文件到可执行程序需要经过编译和链接阶段,而编译又可以细分为预处理、编译、汇编,也就是预处理、编译、汇编、链接四个阶段。
    .png
    让我们再来回顾一下这四个过程,以便于接下来对重载的说明:
    .png

    windows环境下
    对于由多个头文件和多个源文件构成的工程,不同源文件分别经过编译器编译,生成多个目标文件.o结尾,这多个目标文件再经过链接生成可执行程序.exe结尾
    我们需要着重关注的是汇编阶段符号表是如何形成的:
    符号表里是编译期间汇总的的全局行变量,包括全局变量、函数名等,同时为这些变量分配一个地址(可能有效也可能无效)。
    对于C语言来说,被汇总的函数名并没有进行任何修饰,只是把函数名本身汇总了。这里就是C语言不支持重载的原因:
    在同一个源文件中定义的相同的函数名后,到形成符号表这里会出现两个相同的函数名,并且这两个函数还都是有效的,是编译错误。

    image.png

    如果不在同一个源文件中编译链接,分别形成的符号表里各自出现函数名,并且分配一个有效的地址。在接下来的链接阶段,会进行不同符号表的不同符号的合并,此时还是会遇到两个完全相同的函数名并且都是有有效地址,导致链接错误。

    image.png
    linux下gcc编译器(C语言编译器)的函数名修饰为例进行说明
    image.png

    linux下,采用gcc编译完成后,函数名字的修饰没有发生改变,所以同名函数无法区分,也就无法支持重载。

    C++重载分析

    而C++是怎么做的呢?
    C++形成汇编阶段也会形成符号表,对于函数来说,不是简单的直接把函数名放入符号表,而是对函数名进行事先确定好的规则进行修饰,成为一个独一份的名字以此确保不会与其他有函数定义的函数名重复。

    函数名修饰规则与具体的编译器有关,不同的编译器具体实现也不一样,

    接下来以linux下g++编译器的函数名修饰规则为例进行说明:
    image.png

    linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中,只要函数参数不同,修饰出来的名字也就不同,可以区分同名函数,支持了重载 。

    Linux下函数名修饰规则:

    前缀_Z +** 函数名长度** + 每个参数类型的首字母

    int Add(int a, int b) {
    	return a + b;
    }
    
    • 1
    • 2
    • 3

    _Z3Addii

    void func(int a, double b, int *p) {
    	printf("%d %.2lf %p\n", a, b, p);
    }
    
    • 1
    • 2
    • 3

    _ZFuncidPi

    windows下名字修饰约定相比Linux来说比较复杂。

    函数返回值类型相同能重载吗

    我们前面一直在关注函数名和函数参数的信息,往往忽视了函数返回值类型的信息。
    这里牵扯出了两个同名函数,参数个数、类型都相同,只有返回值类型不同,那么这两个函数构成重载吗?

    只有返回值类型不同,不构成重载,因为在函数调用时无法区分要调用哪一个函数,产生了二义性。

    那么,有一个问题:C++中函数返回值类型可以用来修饰函数名吗?

    假设函数返回值类型可以用来修饰函数名
    这样经过了名字修饰后也确实会形成新的独一份的名字,但是对于只有返回值不同的两个函数来说,调用反而是一个问题:即无法让编译器知道调用了哪一个函数。

    如果构成重载,那么能够真正在程序中使用吗?

    不能,原因见前两问。


    结语

    本节主要介绍了C++中的命名空间、缺省参数、重载。希望能够帮助到大家。
    我们下次再见。


    E N D END END

  • 相关阅读:
    UDP通信
    含文档+PPT+源码等]精品基于ssm的足球联赛管理系统的设计与实现vue[包运行成功]计算机Java毕业设计SSM项目源码
    人工智能之Tensorflow编程模型
    修复青龙面板白屏
    【1429】招生管理管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
    ssm+thymeleaf国际化
    C++学习笔记(十五)
    高通导航器软件开发包使用指南(5)
    ddns-go配合aliyun域名解析通过ipv6实现共享桌面
    手机投屏电脑软件AirServer5.6.3.0最新免费版本下载
  • 原文地址:https://blog.csdn.net/weixin_64904163/article/details/127130860