• C++入门【上节】



    前言

    本节知识点安排目的

    1. 补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。
    2. 为后续类和对象学习打基础。

    一、C++关键字(C++98)

    C++总计63个关键字,C语言32个关键字

    ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再
    细讲。

    在这里插入图片描述


    我们先来看看C++如何打印出hello world!

    #include//IO流
    using namespace std;//命名空间namespace
    int main()
    {
    	cout << "hello world!" << endl;
    	return 0;
    }
    
    
    {
    	std::cout<<"hello world!"<<std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这两种方法都可以打印出hello world!,方法二没有加using namespace std;这一段代码也打印出来了

    上面所包含的头文件(iostream)叫做IO流,namespace叫做命名空间

    我们先来看看C++为什么要有namespace命名空间呢?
    我们先来看一段c代码

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

    在这里插入图片描述
    我们可以看到这一段代码没有什么问题,那如果再加上一些代码呢?

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

    在这里插入图片描述
    大家会发现程序崩溃了,这是因为stdlib里面有一个rand函数,而函数名与全局变量rand重复了,所以rand就是被重命名了。而这种重命名定义C语言是无法解决的,只能换一个变量名

    那么如果我们以后工作在提交项目时,如果我们和其他人的项目里面有这种重定义现象很麻烦,只能某一方修改变量名

    在这里插入图片描述

    所以,C++为了解决上面这个问题引入了命名空间


    二、namespace关键字(命名空间)

    namespace作用:避免了命名冲突

    #include
    #include
    
    namespace xxx//命名空间把这个rand包围起来了
    {
    	int rand = 100;
    }
    int main()
    {
    	printf("hello world!\n");
    	printf("%p\n", rand);//此时访问rand默认是库函数里面的
    
    	printf("%d\n", xxx::rand);//这个时候就是访问命名空间里面的rand
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    知识点

    ::这个符号叫做域作用限定符,可以用来访问命名空间里面的内容
    << 这个符号叫做流插入,可以识别数据的类型

    std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中

    1、默认查找:程序在查找变量时优先在局部域查找,找不到再去全局域查找,默认情况下程序不会去命名空间域查找变量

    2、指定查找:比如我们上面的代码std::cout<<"hello world!"<直接去std这个命名空间域里面找cout和endl,如果命名空间域没有变量会直接报错

    3、命名空间域里面的数据是存放在全局域的,只不过命名空间将这些数据给围起来了,我们在命名空间域外面定义的变量名这个时候和内部变量名即使重名也不会起冲突

    4、命名空间不影响变量生命周期,只是限定域,改变编译查找规则

    5、C/C++的头文件会在预处理阶段展开,展开后的代码全部存放在全局域当中

    6、局部变量定义只能在函数内部,因为函数调用的时候会形成函数栈帧,函数调用结束函数栈帧销毁,此时局部变量也跟着销毁,所以在栈帧上的变量叫做局部变量

    下面图中::(域作用限定符)左边是空白,表示在全局域中查找变量a
    在这里插入图片描述

    在这里插入图片描述
    打印第一个rand因为没有使用域作用限定符,所以先在局部域查找变量rand,没有找到再在全局域找rand(这个时候因为rand是头文件展开之后的一个函数,所以rand是函数名,是一个地址,打印出来的就是rand函数地址)

    所以namespace命名空间可以将我们所需要的变量名包围起来,使得库函数里面的函数名等等不会与我们需要的变量名起冲突

    那么上面using namespace std;
    这一段代码是C++为了方便把C++标准库的东西放进std命名空间里面,我们要使用的时候引入这一段代码就可以直接使用命名空间里面的东西;如果不加代码就要使用std::来告诉系统,我们要访问命名空间里面的内容

    #include
    using namespace std;
    int main()
    {
    	int a = 10;
    	char b = 'a';
    	float c = 12.13;
    	double d = 3.1415;
    	cout << a << " " << b << " " << c << " " << d << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    命名空间定义

    1、正常定义

    命名空间中可以定义变量/函数/类型

    namespace bit
    {
    	int rand = 10;
    	int Add(int left, int right)
    	{
    		return left + right;
    	}
    	struct Node
    	{
    		struct Node* next;
    		int val = 10;
    	};
    }
    namespace byte
    {
    	int Add(int left, int right)
    	{
    		return left * 10 + right * 10;
    	}
    	struct Node
    	{
    		struct Node* next=NULL;
    		struct Node* prev=NULL;
    		int val=100;
    	};
    }
    int main()
    {
    	printf("%d\n", bit::Add(1, 2));
    	printf("%d\n", byte::Add(1, 2));
    
    	struct bit::Node cur;
    	struct byte::Node tail;
    	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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在这里插入图片描述

    这里结构体要使用域作用限定符的话要放在struct后面

    2、嵌套定义

    namespace N1
    {
     int a;
     int b;
     int Add(int left, int right)
     {
         return left + right;
     }
     namespace N2
     {
         int c;
         int d;
         int Sub(int left, int right)
         {
             return left - right;
         }
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    这里命名空间嵌套使用的话就使用两个::就可以。而且嵌套定义的命名空间域名字可以相同,也就是上图中N2可以改为N1

    3、命名空间的合并

    我们前面认识了std这个C++官方库内容定义的命名空间域。那么,C++那么多内容会放在一个文件夹或者同一块空间里面吗?

    这是不可能的,C++库里面的内容太多了,是放到多个文件夹里面的,那么既然是多个文件夹,多个命名空间域为什么一个std命名空间域就解决了呢?

    这里就提出来了命名空间是可以合并的概念

    同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

    比如下面的示例:
    ps:一个工程中的1.h和上面2.h中两个N1会被合并成一个
    1.h

    namespace N1
    {
    	int Add(int left, int right)
    	{
    		return left + right;
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.h

    namespace N1
    {
    	int Mul(int left, int right)
    	{
    		return left * right;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    test.cpp

    #include"1.h"
    #include"2.h"
    
    int main()
    {
    	printf("%d\n", N1::Add(10, 20));
    	printf("%d\n", N1::Mul(100, 20));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    这里把Add和Mul都合并到N1里面去了

    注意:只有同级命名空间才能合并在一起。并不是合并文件,而是展开头文件1.h和2.h时,合并命名空间
    其次,合并的时候,命名空间里面不能存在变量名相同,比如将上面的Mul改成Add就会报错,这就相当于我们在同一个域里面定义了两个相同的变量名。

    补充:同一个空间的命名空间也会合并:
    在这里插入图片描述
    这里将头文件展开不就是将1.h和2.h的内容放在test.c的文件里面了吗,所以是可以合并的

    命名空间的使用

    方法一

    int main()
    {
    	std::cout << "hello world!" << std::endl;
    	std::cout << "hello world!" << std::endl;
    	std::cout << "hello world!" << std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们知道std命名空间域里面有许多C++官方库内容,其中cout和endl就在里面,当我们要多次打印的时候每次都要去指定域std里面找太麻烦了,每次都要写std::cout和std::endl,太浪费时间了,所以就有了方法二

    方法二

    using namespace std;
    int main()
    {
    	cout << "hello world!" << endl;
    	cout << "hello world!" << endl;
    	cout << "hello world!" << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们前面见识到了,加上using namespace std;这一行代码就不需要指定命名空间域,不用一直写std::了

    原因就是:std命名空间域里面有着许多C++官方库内容,而using namespacestd;这一行代码把这个std命名空间域给展开了,那么没有了这一层域的保护,我们可以随意使用cout、endl等等C++官方库内容

    但是有利就有弊:我们的确是方便了,但是又回到了C语言的缺点处:std域既然展开了,那万一我们定义了一个全局的cout、endl或者其他名字的变量,不是又和库起冲突了吗?
    在这里插入图片描述
    如果是cout和endl还好,但是如果是一个我们不认识的名称,那就太麻烦了,所以我们有了第三种命名空间使用方法

    方法三

    我们可以将常用的函数或者变量给解封,让这些内容不在std域不就行了?

    using std::cout;
    using std::endl;
    int main()
    {
    	cout << "hello world!" << endl;
    	cout << "hello world!" << endl;
    	cout << "hello world!" << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    这里就把cout和endl拿到std域外面了,我们定义变量名的时候注意即可。如果定义了一个全局变量cout或者endl,我们可以把任意一个变量名放到命名空间内部,这样就有后退的余地了


    三、C++输入/输出

    我们前面认识了cout,这个函数在std域里面,起到输出作用,并且可以自动识别类型;也认识了<<流插入限定符(重载运算符,后面会讲)这个符号
    那么既然有输出函数就有输入函数,有<<符号就有>>符号

    C++的输入函数是cin

    using std::cout;
    using std::cin;
    using std::endl;
    int main()
    {
    	int a;
    	cin >> a;
    	cout << a << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    这里cout和cin分别是consoleout和consolein的简称,我们暂时讲不清楚这两个函数,只需要记住一个是输入一个是输出函数,后面会细讲;

    当然,不是所有情况下cout和cin很方便,比如下面两种情况:

    情况1:打印浮点数,保留小数点后n位,这个cout可以实现,但是太麻烦了,大家直接使用C语言的方法来操作,因为C++兼容C

    using std::cout;
    using std::cin;
    using std::endl;
    int main()
    {
    	double a;
    	cin >> a;
    	printf("%.3f\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    这里发生了精度损失,精度损失不一定是数值变小,还可以是数值变大

    情况2:字符串和数值结合起来打印

    using std::cout;
    using std::cin;
    using std::endl;
    int main()
    {
    	int a;
    	double b;
    	cin >> a;
    	cin >> b;
    	cout << "int " << a << " double " << b << endl;
    	printf("int %d double %f", a, b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    这里熟好孰坏,直接就看出来了,明显C语言更方便

    输出a的ASCLL码值:

    using std::cout;
    using std::cin;
    using std::endl;
    int main()
    {
    	char a;
    	cin >> a;
    	cout << (int)a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    这里强转一下就可以了

    知识点

    1、 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。

    2.、cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。

    3、<<叫做流插入限定符,>>叫做流提取限定符,两个都是重载运算符

    4、对比于C语言的printf和scanf,C++的cout和cin可以自动识别类型(在编译阶段实现自动识别类型),很方便(怎么自动识别类型的下面我们就会讲到)

    5、实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。

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


    四、缺省参数

    概念:

    缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参

    也就是我们可以给形参一个值,我们可以称这个值是形参的缺省值。如果调用函数没有传参,那么形参就使用缺省值;如果有传参,那么就使用传参值

    案例:

    using std::cout;
    using std::cin;
    using std::endl;
    void f(int a = 0)
    {
    	cout << a << endl;
    }
    int main()
    {
    	f();//没有传参时,使用参数的缺省值
    	f(10);//有传参时,使用指定值
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    缺省参数分类

    1、全缺省参数

    顾名思义,全缺省参数就是说每一个形参都有一个缺省值

    using std::cout;
    using std::cin;
    using std::endl;
    void f(int a = 1, int b = 2, int c = 3)
    {
    	cout << "a = " << a << " ";
    	cout << "b = " << b << " ";
    	cout << "c = " << c << endl;
    }
    int main()
    {
    	f();
    	f(10);
    	f(10, 20);
    	f(10, 20, 30);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述
    在传参的时候要从左往右依次传参,不能间隔着传参,会报错

    2、半缺省参数

    半缺省参数必须从右往左依次来给形参缺省值,不能间隔着给

    void f(int a, int b, int c = 20, int d = 4)
    {
    	cout << "a = " << a << endl;
    	cout << "b = " << b << endl;
    	cout << "c = " << c << endl;
    	cout << "d = " << d << endl;
    
    }
    int main()
    {
    	//f();//会报错
    	//f(10);//会报错
    	f(10, 20);
    	f(10, 20, 30);
    	f(10, 20, 30, 40);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    半缺省参数的形参没有给缺省值时,传参过程一定要给参数,不然会报错

    缺省参数使用案例

    我们知道头文件一般用来存放结构体的定义和函数的声明的
    2.h

    #include
    #include
    #pragma once
    namespace bit
    {
    	typedef struct Stack
    	{
    		int* a;
    		int top;
    		int capaicty;
    		//....
    	}ST;
    
    	// 缺省参数不能在函数声明和定义中同时出现,在声明中给
    	void StackInit(ST* ps, int defaultCP = 4);
    	void StackPush(ST* ps, int x);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.cpp

    #include"2.h"
    namespace bit
    {
    	// 缺省参数不能在函数声明和定义中同时出现
    	void StackInit(ST* ps, int defaultCP)//这里的defaultCP不能给缺省值
    	{
    		ps->a = (int*)malloc(sizeof(int) * defaultCP);
    		assert(ps->a);
    		ps->top = 0;
    		ps->capaicty = defaultCP;
    	}
    
    	void StackPush(ST* ps, int x)
    	{}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    缺省参数不能在函数声明和函数定义中同时出现,只能在函数声明中出现,在函数定义出现会报错

    知识点

    1、传参时,必须从左往右按顺序传参,不能隔着传参

    2、半缺省参数必须从右往左依次来给出,不能间隔着给

    3、缺省参数不能在函数声明和定义中同时出现,如果函数的声明和定义分开的话,只能在声明中给

    4、缺省值必须是常量或者全局变量,一般都是常量

    5、半缺省参数的形参没有给缺省值时,传参过程一定要给参数,不然会报错


    五、函数重载

    概念:

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

    简单来说:c语言不支持两个函数名相同。但是C++里面,如果同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,那么C++就支持多个函数名相同,构成函数重载(注意,构成函数重载不代表就能够调用函数成功)

    我们来看看案例

    参数个数不同

    void f(int a, int b)
    {
    	cout << a + b << endl;
    }
    void f(int a, int b, int c)
    {
    	cout << a + b + c << endl;
    }
    int main()
    {
    	int a, b, c;
    	f(10, 20);
    	f(1, 2, 3);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    我们可以看到,函数名相同,但是函数参数个数不同,程序能够正确的自动匹配到对应函数,也就是说这两个函数构成重载函数

    参数类型不同

    void f(int a, int b)
    {
    	cout << a + b << endl;
    }
    void f(double a, double b)
    {
    	cout << a + b << endl;
    }
    int main()
    {
    	int a, b;
    	double c, d;
    	f(10, 20);
    	f(10.10, 20.20);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    同上,这里参数类型不同,程序也能够正确的自动匹配到对应函数

    形参类型顺序不同

    void f(int a, double b)
    {
    	cout << a + b << endl;
    }
    void f(double a, int b)
    {
    	cout << a + b << endl;
    }
    int main()
    {
    	int a, d;
    	double c, b;
    	f(10, 20.20);
    	f(10.10, 20);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    这里就是参数类型顺序不同,程序也能够正确的自动匹配到对应函数

    注意,是类型顺序不同,不是参数变量名顺序不同

    案例

    
    //构成函数重载 -- f()调用会报错,存在歧义
    void f()
    {
    	cout << "f()" << endl;
    }
    
    void f(int a = 0, char b = 1)
    {
    	cout << "f(int a,char b)" << endl;
    }
    
    int main()
    {
    	f(10);
    	f(10, 20);
    	f(); // 歧义  二义性
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    上面的f()就是我前面说的,函数构成重载,但是不一定能够调用成功,f()就存在歧义,C++官方把这种歧义称为二义性

    C++支持重载的原理(重点)

    我们知道C语言不支持重载,C++支持重载,那么是怎么支持的?

    不管是C语言还是C++,在调用函数的时候都是通过函数名来查找该函数的地址,然后通过地址来调用函数
    之前我们在C语言学习了符号表这一节,符号表是由函数名及其地址构成的:

    C语言中因为不支持函数重载,所以同名的函数名区分不了;
    在这里插入图片描述

    而C++的符号表原本也是函数名及其地址构成的,但是C++又要支持函数重载,所以对比C语言符号表,C++的符号表发生了改变,这个改变就用来区分重载函数

    区分重载函数的方法就与函数重载的规则有关如果同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同
    我们只需要通过函数的参数不同,那么它对应的符号表函数名发生变化,就可以找到对应的函数就行调用,这种方法就叫做——函数名修饰规则

    那么我们就来看看windows下面函数名修饰规则:
    在这里插入图片描述
    可以看到windows下面的函数名修饰规则很难看懂,那么我们来看看linux在g++编译器下面的函数名修饰规则:

    在这里插入图片描述
    上图右边方框内部的两个代码就是实施函数名修饰规则之后C++符号表内部的函数名。
    这里的函数名都是有具体意义的:
    上面的_Z3Addii——前面的_Z类似于前缀,在linux的g++编译器中采用函数名修饰规则都会带这个前缀;数字3表示函数名的长度;Add就是函数名;ii表示该函数有两个int型的参数
    _Z4funcidPi——_Z是前缀;4是函数名长度;func是函数名;i表示函数第一个参数是int型;d表示函数第二个参数是double类型;Pi表示函数第三个元素是指针类型


    案例:

    在这里插入图片描述

    总结就是一句话:函数参数不同,函数名修饰规则导致符号表内部的函数名不同,就可以正确找到对应的函数,从而构成了重载函数(C++把函数名带参数修饰了一下)

    目前就只能解释到这里了,剩下的知识只能等到后面再讲了

    返回值不同为什么不能够成重载?(面试题)

    int f(int a, int b)
    {
    	cout << "f(int a,char b)" << endl;
    	return 0;
    }
    char f(int b, int a)
    {
    	cout << "f(int a,char b)" << endl;
    	return 'A';
    }
    // 返回值不同,不构成重载原因,并不是函数名修饰规则
    // 真正原因是调用时的二义性,无法区分,调用时不指定返回值类型
    int main()
    {
    	f(1, 1);
    	f(2, 2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    因为在函数调用的时候,我们能够指明参数的类型,但是我们不能够指明返回值的类型。这样在两个函数参数相同,返回值不同的情况下,我们就无法确定调用哪一个函数,产生了二义性。所以返回值不能构成函数重载

    补充知识点

    cout<cout自动识别类型的本质是函数重载支持的


    六、引用

    概念

    引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

    类型& 引用变量名(对象名) = 引用实体;

    在类型后面加上取地址符号——类型+&

    注意:引用类型必须和引用实体是同种类型的

    1、引用特性

    · 1. 引用在定义时必须初始化
    · 2. 一个变量可以有多个引用
    · 3. 引用一旦引用一个实体,再不能引用其他实体

    当我们每次要使用引用的时候,一定要记得对引用初始化,不然会报错!!!

    案例1:引用未初始化

    在这里插入图片描述

    引用指定实体之后,不能够再次引用其他实体

    案例2:引用多次

    在这里插入图片描述
    这里可以看到ra和m的值被改为了200,但是ra还是引用实体m的,所以引用指定一个实体之后,不能再次引用其他实体,这也是为什么引用无法完全替代指针的原因之一

    接下来我们看看引用的应用场景在哪里

    2、使用场景

    案例1:基础使用方法

    int main()
    {
    	int a = 10;
    	int& ra = a;
    	int& y = a;
    	int& x = y;
    	x++;
    	y++;
    	a++;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    案例2:作为参数交换数据
    有了引用我们在交换数据不需要传地址了

    void Swap(int& a, int& b)
    {
    	int tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a, b;
    	cin >> a;
    	cin >> b;
    	Swap(a, b);
    	cout << "a=" << a << endl;
    	cout << "b=" << b << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    案例3:替换二级指针

    void SlistPushBack(struct ListNode** pphead, int n)//以前的二级指针
    {}
    void SlistPushBack(struct ListNode*& phead, int n)//现在的引用
    {}
    int main()
    {
    	struct ListNode* plist = NULL;
    	SlistPushBack(plist, 1);
    	SlistPushBack(plist, 2);
    	SlistPushBack(plist, 3);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    案例4:引用做返回值

    这个案例很重要,我们先来回顾一下之前的知识

    就我们当前所学内容,我们知道数据存放区域有4个:
    第一个:栈区。函数调用时在栈区上面开辟空间,调用结束,栈自动销毁
    第二个:堆区。动态内存开辟在堆区上面开辟,要进行手动释放动态内存
    第三个:静态区。又称数据段,我们后面会讲。用来存放全局变量和static修饰的数据
    第四个:常量区。又称代码段。这个也后面讲
    其中,栈区是向下生长,堆区向上生长
    在这里插入图片描述

    我们先来看看一段代码:(这里在敲代码的时候没注意主函数和函数变量名相同)

    int Test()
    {
    	int a = 10;
    	a++;
    	return a;
    }
    int main()
    {
    	int a = Test();
    	cout << a << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里函数调用结束之后a不应该销毁了吗?怎么被带回主函数的呢?
    其实我们前面看见过答案,可能大家都忘记了:

    在这里插入图片描述
    所以,在大多数情况下,函数调用结束,寄存器会拷贝返回值,然后带回到主函数内部

    那么,如果用一个引用做返回值呢?

    在这里插入图片描述

    这里可以看到,引用做返回值的时候,有一个static修饰变量a,如果没有会出现什么问题呢?

    static修饰了局部变量之后,局部变量存放在了静态区,局部变量的生命周期延长。所以函数调用销毁之后,变量a一直存在,直到退出或者销毁程序。所以主函数可以轻松拿到返回值。而没有这个static,此时函数栈帧的空间就销毁,变量a就随着函数调用结束销毁了。

    空间销毁的意义是什么?

    空间销毁后该空间还存在内存中,只是该空间的使用权不是我们的了,归还给操作系统了,那么我们在该空间的数据不被保护了。
    我们还是可以去访问该空间(这里就是越界访问了),但是里面的数据变没变、有没有被清除、有没有被其他数据覆盖,这是我们无法确定的

    所以,没有static修饰直接使用引用做返回值,带回到主函数的返回值是不确定的

    我们看来看一段代码就知道了:

    在这里插入图片描述
    这里第二次打印ret结果出现了一个很大的数值,这是因为cout也是一个函数,调用cout函数时覆盖了第一次调用Count原来的函数栈帧;
    第一次调用的时候ret先被取到,然后调用cout函数打印结果,所以第一次打印的时候ret还是可以取到的,然后就被cout函数给覆盖了;
    而第三次打印是因为变量x刚好覆盖了ret,所以导致ret的值变成了100

    注意:
    上面代码的三个打印结果不一定是我这样的,有的人可能全部都是随机值,有的人也有可能都是1等等等等,所以,大家一定要记住,引用做返回值,要带上static就行修饰,不然结果有可能会是错误的;


    3、传引用做返回值的作用

    1、减少拷贝,提高效率
    2、修改返回值
    3、引用做参数

    因为不用引用做返回值要拷贝返回值到寄存器,才能把返回值带回到主函数

    但是,为了4个字节的返回值就使用引用,有意义吗?
    答案是没有意义,但是别急,看看下面的代码:

    传值和传引用做返回值效率比较:

    #include 
    struct A{ int a[10000]; };
    A a;
    // 值返回A 
    A TestFunc1() { return a;}
    // 引用返回
    A& TestFunc2(){ return a;}
    void TestReturnByRefOrValue() 
    {    // 以值作为函数的返回值类型    
    	size_t begin1 = clock();    
    	for (size_t i = 0; i < 100000; ++i)        
    		TestFunc1();    
    	size_t end1 = clock();
    	// 以引用作为函数的返回值类型    
    	size_t begin2 = clock();    
    	for (size_t i = 0; i < 100000; ++i)        
    		TestFunc2();    
    	size_t end2 = clock();
    	// 计算两个函数运算完成之后的时间    
    	cout << "TestFunc1 time:" << end1 - begin1 << endl;    
    	cout << "TestFunc2 time:" << end2 - begin2 << endl;
    }
    int main()
    {
    	TestReturnByRefOrValue();
    	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
    • 25
    • 26
    • 27

    在这里插入图片描述

    通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

    4、传值、传引用做参数

    我们知道传值的时候,形参是实参的一份临时拷贝,改变形参不能改变实参;
    而传引用时,对形参的修改就可以改变实参

    所以,对于引用做参数而言:

    1、减少拷贝,提高效率
    2、输出型参数,改变形参就能改变实参

    传值和传引用做参数效率比较:

    #include 
    struct A{ int a[10000]; };
    void TestFunc1(A a){}
    void TestFunc2(A& a) {}
    void TestRefAndValue()
    {
    	A a;
    	// 以值作为函数参数    
    	size_t begin1 = clock();
    	for (size_t i = 0; i < 100000; ++i)        
    		TestFunc1(a);    
    	size_t end1 = clock();
    	// 以引用作为函数参数    
    	size_t begin2 = clock();    
    	for (size_t i = 0; i < 100000; ++i)        
    		TestFunc2(a);    
    	size_t end2 = clock();
    	// 分别计算两个函数运行结束后的时间    
    	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;    
    	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
    }
    int main()
    {
    	TestRefAndValue();
    	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
    • 25
    • 26

    在这里插入图片描述
    可以看到效率明显提高了,引用做返回值确实能够提高效率。

    接下来我们来证明前面说的:函数构成重载,但是调用不成功

    void Swap(int a, int b)
    {
    	int tmp = a;
    	a = b;
    	b = tmp;
    }
    void Swap(int& a, int& b)
    {
    	int tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a = 10;
    	int b = 20;
    	Swap(a, b);
    	cout << a << endl;
    	cout << b << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这一段代码就是两个函数构成重载,但是调用不会成功
    在这里插入图片描述


    5、常引用

    int main()
    {
    	int a = 10;
    	int& ra = a;//权限不变
    	const int& rra = a;//权限缩小
    	rra++;//rra被const修饰不可修改,但是a可以修改
    	a++;
    	cout << rra << endl;
    	
    	//指针和引用赋值过程中,权限可以平等(平移),可以缩小,但是不能放大
    	const int b = 10;
    	int& rb = b;//权限放大了,不行
    	
    	const int& rb = b;//权限不变
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里const修饰的变量不能直接引用,在指针和引用赋值过程中,权限可以平等(平移),可以缩小,但是不能放大。这里的变量b结果const修饰之后只能可读,不能可写了

    注意

    权限的平等(平移),缩小,不能放大,仅限于指针和引用的赋值过程中

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    补充知识点

    在这里插入图片描述

    数据在进行类型转换,整型提升,截断时,会产生一个临时变量,临时变量具有常属性
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述


    6、引用和指针的区别

    在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

    int main() 
    {
    	int a = 10; 
    	int& ra = a;
    	cout << "&a = " << &a << endl; 
    	cout << "&ra = " << &ra << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    但是实际在底层实现上引用是有空间的,因为引用是按照指针的方式来实现的

    我们可以转到反汇编去看一下:

    在这里插入图片描述
    我们可以看到,引用和指针的反汇编代码是一样的,也就是说引用和指针底层一样。通过引用是按照指针的方式来实现的结论,和上面的反汇编代码,可以得出引用底层实现也是开辟空间了的

    得出结论:

    引用在语法上面是别名,不占用空间
    引用在底层是用指针实现的,要占用空间

    引用和指针的不同点:

    1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

    2. 引用在定义时必须初始化,指针没有要求

    3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

    4. 没有NULL引用,但有NULL指针

    5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64平台8个字节)

    6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

    7. 有多级指针,但是没有多级引用

    8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

    9. 引用比指针使用起来相对更安全

    七、内联函数

    我们先来复习一下C语言中,定义一个两个变量相加的宏函数,是怎么定义的?

    # define Add(x,y) ((x)+(y))
    
    • 1

    答案是这样定义的,那么为什么宏定义不带分号,为什么((x)+(y))要有外面的括号、里面的括号呢?
    我们来一一分析

    情况一:宏定义不带分号

    因为宏定义在预处理阶段就要进行替换,而如果带分号就会把分号也给替换进调用宏的地方

    在这里插入图片描述

    情况二:宏定义不要外层括号

    这种不要外层分号容易遇到优先级有关的计算问题
    在这里插入图片描述

    情况三:宏定义不要里面的内层括号

    这里也是因为运算优先级的问题
    在这里插入图片描述

    结果上面的几个例子我们对宏又有了一定的了解

    既然有了宏有为什么C++要弄一个内联函数inline呢?

    因为宏有着很明显的缺点

    宏的缺点

    1、宏不能调试

    我们知道程序在编译链接完成之后,我们才能够调试,但是宏在预处理阶段就完成了替换,导致我们不能够调试了,这个缺点很致命

    2、宏没有类型安全检查

    本来我们需要传一个double类型的变量,结果我们传了一个int类型的变量,但是编译的时候检查不出来类型错误

    3、宏容易写错

    我们在写宏的时候,很容易少了括号,加了分号…导致宏写错了

    由于宏的种种原因,C++为了解决宏存在的缺陷,提出了内联函数inline

    一些书籍中提出了:用inline替换宏,const,enum来替换宏里面的变量

    内联函数的概念

    inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销,内联函数提升程序运行的效率

    简单来说:
    在函数名前面加上inline关键字进行修饰,函数就变成了内联函数,在编译期间编译器会用函数体替换函数的调用,进行inline展开。
    而内联函数的优点是:没有函数调用建立栈帧的开销,内联函数提升程序运行的效率

    我们来看看内联函数于非内联函数的区别:

    1、在release模式下,查看编译器生成的汇编代码中是否存在call Add
    2、我们的编译器如果是在debug版本下面的话需要进行配置一下,因为debug版本默认支持可以调试,但是内联函数展开是不支持调试的,所以我们要配置一下debug版本

    配置debug版本(VS2013):
    在这里插入图片描述

    如果有call Add汇编代码就表示没有展开,该函数不是内联函数;反之,则是内联函数

    下面两张图片是非内联函数
    在这里插入图片描述

    在这里插入图片描述

    下面两张图是内联函数
    在这里插入图片描述
    在这里插入图片描述

    通过上面的比较可以看到,内联函数展开了,Add函数并没有进行调用生成函数栈帧。这样提高了程序运行效率

    内联函数的特性(重点)

    特性1

    inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用
    缺陷:可能会使目标文件变大;
    优势:少了调用开销,提高程序运 行效率。

    这里的空间换时间的空间是:可执行程序的大小(静态库/动态库)

    特性2

    inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建 议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不 是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为 《C++prime》第五版关于inline的建议:
    在这里插入图片描述

    递归我们就不说了,应该99%的编译器都不支持递归内联。我们就举一个长函数的例子:

    inline int Add(int a, int b)
    {
    	int x = a + b;
    	x = a + b;
    	x = a + b;
    	x = a - b;
    	x = a + b;
    	x = a + b;
    	x = a * b;
    	x = a + b;
    	x = a + b;
    	x = a + b;
    	x = a / b;
    	x = a + b;
    	x = a + b;
    	x = a + b;
    	x = a + b;
    	return x;
    }
    int main()
    {
    	int a = Add(10, 20);
    	cout << a << 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
    • 25
    • 26

    在这里插入图片描述
    我们可以看到一个二三十行的函数使用inline编译器都不会讲该函数展开,所以内联一般用于规模小,频繁调用的函数

    所以说: inline对于编译器而言只是一个建议,函数到底展不展开,是否内联还是取决于编译器

    面试题

    提问:为什么函数过长展开会失败(内联失效)?

    答:因为函数过长内联会引起代码膨胀

    举例说明:

    假如我们现在有一个func函数,函数里面有30行指令,现在有10000个地方要调用func函数。
    函数inline展开,那么将会有10000*30=30W行代码指令(10000个地方调用,每次调用将func函数的30行指令展开)
    函数inline不展开,那么将会有10000+30=10030行代码指令(10000个地方调用,也就是10000行call func指令,加上原本的30行func内部指令)

    在这里插入图片描述
    上面我们计算得出来的指令影响可执行程序的大小,也就是安装包的大小。我们都知道安装包大了不好,会导致下载效率很慢

    特性3

    inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址 了,链接就会找不到。

    举个简单的例子:
    1.h

    #pragma once
    #include
    
    using std::cout;
    using std::endl;
    using std::cin;
    
    inline int Add(int a, int b);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.cpp

    #include"1.h"
    int Add(int a, int b)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    test.cpp

    #include"1.h"
    int main()
    {
    	int a = Add(10, 20);
    	cout << a << endl;
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果:
    在这里插入图片描述
    解析:

    我们执行程序的时候头文件会展开,那么text.cpp里面存在Add函数的声明。在只有声明的条件下编译器通过Add的返回值,函数名,参数等内容查看调用的Add函数是否匹配,匹配就可以通过。如果Add函数的定义在text.cpp里面,那么编译器可以直接找到Add函数的地址,直接调用(call Add函数);如果text.cpp只有函数的声明,那么等到链接的阶段,编译器直接通过函数名(C++通过函数修饰名)去符号表找该函数。

    本来没有inline修饰的话,Add函数在1.cpp文件中通过预处理、编译、汇编…一系列操作转换成对应的指令,然后把函数名及其地址汇总成为符号表,这个时候编译器通过函数名就会找到函数的地址,从而进行调用。
    但是,因为有了inline修饰,那么在1.cpp里面的Add被编译器认为是内联函数。因为内联函数是哪个地方调用谁就把该地方替换成函数,然后进行指令展开。此时的text.cpp里面只有函数声明,没有函数实现(也就是没有函数的定义),所以不能够展开,只能够靠链接的时候通过函数名去找Add函数,但是因为没有call Add函数这一条指令,也就导致在1.cpp的Add函数不会进行调用、不会建立函数栈帧、不会生成Add函数的汇编指令…,那么就导致了符号表里面没有Add函数及其对应地址,这个时候编译器找不到Add函数,链接失败,也就导致了链接错误

    结论

    如果一个函数的声明和定义分离,并且在声明处使用了inline来修饰函数。那么,不管该函数是不是内联函数,只要有inline修饰的函数都不能在符号表中存储函数名及其地址,调用时一律会链接出错

    所以,我们在使用内联函数的时候,函数的声明和定义全部的放在.h文件里面实现。因为使用的地方会包含.h文件,那么就相当于拿到了函数的实现,可以直接展开了,不像上面那样只有声明,没有定义,想展开展不开

    面试题

    宏的优缺点?

    优点:
    1.增强代码的复用性。
    2.提高性能。

    缺点:
    1.不方便调试宏。(因为预编译阶段进行了替换)
    2.导致代码可读性差,可维护性差,容易误用。
    3.没有类型安全的检查 。

    C++有哪些技术替代宏?

    1. 常量定义 换用const enum
    2. 短小函数定义 换用内联函数

    八、auto

    随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

    1. 类型难于拼写
    2. 含义不明确导致容易出错

    auto简介

    在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的
    是一直没有人去使用它,大家可思考下为什么?

    C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一
    个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

    知识点

    1、使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

    2、auto与指针和引用结合起来使用。用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

    3、在同一行定义多个变量。当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

    auto不能推导的场景 :

    1、auto不能作为函数的参数(auto不能作为形参类型,因为编译器无法对a的实际类型进行推导,这就导致了在开辟函数栈帧的时候,无法确定开辟多大的内存空间)
    2、auto不能直接用来声明数组
    3、 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
    4.、auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。


    九、typeid

    模板:

    typeid(变量).name()

    我们目前只要会使用这个typeid就行,具体的后面会讲到

    案例:
    在这里插入图片描述


    十、基于范围的for循环(C++11)

    在这里插入图片描述
    注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

    范围for的使用条件 :

    1. for循环迭代的范围必须是确定的对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

    注意:以下代码就有问题,因为for的范围不确定:

    void TestFor(int array[])
    {
        for(auto& e : array)
            cout<< e <<endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)

    十一、nullptr

    NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

    #ifndef NULL
    #ifdef __cplusplus
    #define NULL   0
    #else
    #define NULL   ((void *)0)
    #endif
    #endif
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何
    种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

    void f(int)
    {
     cout<<"f(int)"<<endl;
    }
    
    void f(int*)
    {
     cout<<"f(int*)"<<endl;
    }
    
    int main()
    {
     f(0);
     f(NULL);
     f((int*)NULL);
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
    在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void
    *)0。

    注意:

    1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
    2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
    3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

    总结

    到这里我们也算是初步认识了C++了,对C++有了一点点的认识了,未来还有许多知识等着我们去掌握,让我们一起加油吧!

  • 相关阅读:
    MyBatis的各种查询功能
    常坐飞机的你,为什么老惦记着“升舱”?
    flink 复postgresql数据库数据
    sql索引分析-插入了 a、b、c、d 四个字段作为索引,只要带上了a,那么任何排列的组合,都可以走索引。
    并查集合并、计算集合个数、每个集合的元素
    GitHub上9款美观大气的后台管理系统,基于但不限于vue、react、Angular、Bootstrap框架
    Spring学习(8) AOP
    以效率为导向:用ChatGPT和HttpRunner实现敏捷自动化测试(二)
    20220814NOI模拟赛--考后总结
    redis的性能管理、主从复制和哨兵模式
  • 原文地址:https://blog.csdn.net/kdjjdjdjjejje128/article/details/126849636