IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C++初阶-C++入门】舒服了... -> 正文阅读

[C++知识库]【C++初阶-C++入门】舒服了...

前言

此篇,就正式进入C++的学习了。

C++,高级语言,从名字也能窥见一二,是C语言的 “升级版”。它对C语言中许多空白的地方进行填补,不足的地方进行优化,允许我们 “面向对象编程”。因此我们在学习过程中可以多多对比C++和C。它的优势也和C一样,可以直接操控硬件,相比其他高级语言较底层,因此适合做游戏开发、服务器开发等等偏后端的事儿。

今天是C++学习的第一站,我们先来学学基础语法第一课——C++入门

主要内容有:

  • 命名空间
  • 缺省参数
  • 函数重载
  • 引用
  • 内联函数
  • auto
  • 范围for
  • nullptr

1. 命名空间

1.1 C的命名缺陷

C语言中关于命名,同个域中不允许变量、类型同名,也不允许函数同名。这会带来一些问题

  1. 自己定义的变量、函数,可能会和库里的冲突
  2. 做大项目的时候,也常出现命名冲突问题
#include <stdio.h>

int rand = 10;

int main()
{
	printf("%d\n", rand);

	return 0;
}

:10

现在可以正常输出,那我这样呢?

#include <stdio.h>
#include <stdlib.h>

int rand = 10;

int main()
{
	printf("%d\n", rand);

	return 0;
}

在这里插入图片描述

此处就是自己定义的变量和库里的冲突,说到这,我们再问一个问题:

问:编译器是怎么找变量的?

#include <stdio.h>
//#include <stdlib.h>

int rand = 10;

int main()
{
	int rand = 20;

	printf("%d\n", rand);

	return 0;
}

:20

局部 ==> 全局 ==> 找不到则报错

C++为了解决这个问题,提出了命名空间的概念…

1.2 命名空间

namespace 命名空间是C++内置的关键字

命名空间的定义

先看看它用起来是什么样的

#include <stdio.h>

namespace bacon
{
	int pig = 10;
}

int main()
{
	printf("%d\n", bacon::pig);
}

:10

语法:namespace [命名空间名]

有点像结构体啊,是的,结构体和命名空间的本质都是封装,只不过结构体封装的是类型,命名空间封装的是“名字”,它的本质是:

  • 隔离 全局变量、类型、函数

  • 并改变编译器查找规则

改变编译器查找规则?代码里的 : : 是什么东西?

命名空间的使用

域作用限定符

语法:mynamespace : : numbers

? 命名空间 : : 成员

上面的代码中, bacon : : pig 的含义就是,在命名空间bacon中查找pig成员,这也是“改变编译器查找规则”的意思

编译器查找规则

  • 不指定命名空间:局部 ==> 全局 ==> 报错(不指定bacon,编译器查找的时候就会当作没看到bacon)

  • 指定命名空间

    • 在命名空间找

      printf("%d\n", bacon :: pig);
      
    • 在全局找

      printf("%d\n", :: pig);
      

命名空间的使用分以下三种:

  1. 使用时指定成员

    printf("%d", bacon :: pig);
    
    :10
    

    隔离效果最佳,但是使用较麻烦

  2. 引入指定成员

    using bacon :: pig;
    
    printf("%d", pig);
    
    :10
    

    常用的可以引入

  3. 引入整个命名空间

    using namespace bacon;
    
    printf("%d ", pig);
    printf("%s", song);
    
    :10 Mojito
    

    隔离失效,平时练习可以这么用,做项目之类的就别了

命名空间的嵌套定义

namespace bacon
{
	int pig = 10;

	namespace idol
	{
		namespace jay
		{
			const char* song = "Mojito";
		}
	}
}

int main()
{
	printf("%s\n", bacon::idol::jay::song);
}

:Mojito

2. hello world

了解完命名空间,我们进一步学习 hello world 程序

#include <iostream>

using namespace std;

int main()
{
	cout << "hello world" << endl;
	
	return 0;
}

:hello world
  • #include <iostream> :使用 cin/cout 进行标准输入输出的时候,需要包含 <iostream> 头文件

  • using namespace std:

    • std:C++标准库的命名空间名,标准库的定义实现都在这个命名空间中
    • cin/cout :
      • 都是标准库中的函数,使用它们要指定命名空间
      • 自动识别类型(其实是一种函数重载,后面谈)
  • << :流插入运算符

    >>:流提取运算符

问:那想控制输出格式呢?

用printf方便就用呗!哪个方便用哪个

3. 缺省参数(默认参数)

缺省参数,就是在声明函数的某个参数的时候,为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数

缺省参数也分 全缺省 和 半缺省

  • 全缺省

    #include <iostream>
    
    using namespace std;
    
    int f1(int e1 = 10, int e2 = 20)
    {
    	return e1 + e2;
    }
    
    int main()
    {
    	int ret = f1();
    
    	cout << ret << endl;
    
    	return 0;
    }
    
    :30
    
  • 半缺省:只能从右往左连续缺省(需要缺省的往后放)

    int f2(int e1, int e2 = 20)
    {
    	return e1 + e2;
    }
    
    int main()
    {
    	int ret = f2(100);
    
    	cout << ret << endl;
    
    	return 0;
    }
    
    :120
    

注意:

  • 缺省参数不能在函数的声明和定义中同时出现(我们放在声明里就好)

    • 如果声明定义的缺省值不同,编译器不知道用哪个了
  • 缺省参数一般是 常量 或 全局变量

4. 函数重载

函数重载(fuction overload),指 我们可以通过传不同的参数来区分同名的函数。

简单来说,“一词多义”,一个函数名能调用执行不同的功能;反过来说,不同功能的函数可以同名。只需要:参数列表不同(类型、个数、顺序)。简单归纳:

构成函数重载:

  • 函数名相同
  • 函数参数列表不同
#include <iostream>

using namespace std;

int Add(int e1, int e2)
{
	return e1 + e2;
}

double Add(double e1, double e2)
{
	return e1 + e2;
}

int main()
{
	int i1 = 10;
	int i2 = 20;
	double d1 = 1.1;
	double d2 = 2.2;

	int reti = Add(i1, i2);
	double retd = Add(d1, d2);

	cout << reti << endl;
	cout << retd << endl;

	return 0;
}30
3.3

cin/cout 的自动识别类型也是函数重载,通过参数列表区分不同的输出类型

在这里插入图片描述

函数重载的二义性

现有两个构成重载的函数,调用时我们传的参数无法让编译器明确区分两个函数,我们称这两个构成重载的函数有 二义性

#include <iostream>

using namespace std;

void f()
{
	cout << "f()" << endl;
}

void f(int e1 = 0, int e2 = 0)
{
	cout << "f(int e1 = 0, int e2 = 0)" << endl;
}

int main()
{

	return 0;
}

不调用并编译,能编译成功,说明构成重载

现在来调用一下看看

#include <iostream>

using namespace std;

void f()
{
	cout << "f()" << endl;
}

void f(int e1 = 0, int e2 = 0)
{
	cout << "f(int e1 = 0, int e2 = 0)" << endl;
}

int main()
{
	f();

	return 0;
}

在这里插入图片描述

实际使用中要注意避免产生二义性

函数重载的简单原理

函数重载的原理不适合现阶段解剖,我们浅浅了解一下就够

我们知道,函数调用就是找到函数名对应的地址,执行地址处的函数体,但,相同的函数名怎么找到不同的地址?

虽然C++中构成函数重载的函数名看起来一样,但其实不尽相同…

函数名修饰

我们先来回忆一下 依函数声明找函数定义 的过程,

  • Add.c:声明Add函数
  • test.c:调用Add函数

程序从源文件到可执行程序:

源文件 ==预处理==> ==编译==> ==汇编==> ==链接==> 可执行程序

  • 预处理:展开头文件,替换宏,过滤注释…
  • 编译:不同源文件隔离编译,期间进行 语法分析、词法分析、语义分析、符号汇总
  • 汇编:生成 .o 目标文件 、形成符号表
  • 链接:合并段表、合并符号表、符号表重定位

*段表:把逻辑段映射到物理内存区

在这里插入图片描述

*符号表:编译过程时,源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址

再来看:test.c 中调用了 Add,但编译器在编译后链接前,要找函数定义的时候,发现 test.o 的目标文件内没有函数地址(找不到函数定义),就到 Add.o(其他目标文件) 中找,找到后链接(找不到就是链接错误)

接下来再看C和C++中,找函数定义时的区别:

(由于win的vs下函数名修饰太复杂,Linux下的gcc/g++就很好理解,所以下面用gcc演示)

int Add(int a, int b)
{
	return a + b;
}

void func(int a, double b, int* p)
{}

int main()
{
	Add(1, 2);
	func(1, 2, 0);
	return 0;
}
  • C语言编译器

在这里插入图片描述

  • C++ 编译器

在这里插入图片描述

可以看到,C编译器编译后,函数名不变;C++编译器编译后函数名被修饰了:[ _Z + 函数名长度 + 函数名 + 参数类型首字母 ]

我们使两个函数构成重载时,参数列表的不同,已经决定了两个函数是“不一样的函数”了,怎么说?

不同的参数列表,使函数名被修饰成不同的样子,有了修饰后 根本意义上不同的函数名,这样一来,C++编译器通过函数名修饰规则,就能找到对应代码

问:返回值类型不同能不能构成函数重载?

不能的话,那我们在函数名修饰中带上返回值类型不就好了吗?

:不能构成,而且也并不是因为函数名修饰,而是 调用时的二义性——

调用的地方不能控制返回值的类型,我调用返回值为int的,你给我调用返回值为char的,那可不行

归纳一下,函数重载:

  • 是参数列表不同的同名函数,能执行不同代码
    • 原理是函数名修饰规则,让编译器有了分辨的能力
  • 返回值必须相同
    • 不同的返回值产生二义性
  • 参数列表不能有二义性
    • 如 全缺省 和 无参数 的函数构成重载,调用时就会有二义性

5. 引用

引用,一种数据类型,像是变量的同位语,或是给它取别名,一块空间的不同名字

比如: 周杰伦,JayChou,亚洲音乐天王…

周杰伦就是JayChou,JayChou就是亚洲音乐天王,没有区别

#include <iostream>

using namespace std;


int main()
{
	int i = 10;
	int& ri = i;
	int& rri = i;

	cout << &i << endl;
	cout << &ri << endl;
	cout << &rri << endl;

	return 0;
}

:02D8FBE0
02D8FBE0
02D8FBE0

特性:

  • 一个实体可以有多个引用
  • 引用必须初始化,且初始化后不能更改引用的实体
    • 所以C++的引用代替不了指针,二者相辅相成
#include <iostream>

using namespace std;

int main()
{
	int i1 = 10;
	int i2 = 20;
	int& ri = i1;

	ri = i2;//赋值操作,不是引用新实体

	cout << ri << endl;

	return 0;
}

:20

5.1 引用的应用场景

作参数

#include <iostream>

using namespace std;

int f(int& ri)
{
	return ri *= 10;
}

int main()
{
	int i = 10;
	
	int ret = f(i);

	cout << ret << endl;

	return 0;
}

ri 接收到参数 i 后就成了 i 的别名,对 ri 操作 == 对 i 操作

引用作参数的优势:

  1. 减少拷贝,提高效率
  2. 引用参数是输出型参数——函数中修改形参,实参也跟着改变

作返回值

//传值返回
int f1()
{
	int n1 = 1;
	return n1;
}

//引用返回
int& f2()
{
	int n2 = 2;
	return n2;
}

先回忆一下 传值返回…

传值返回

:创建一个和返回值类型同类型的临时变量(之后称作tmp)==> 将 n1 放进 tmp ==> 销毁栈帧 ==> 通过临时变量带回返回值

这样一来就有一层 拷贝

int tmp = n1;
//销毁栈帧
return tmp;

(tmp小就创建在寄存器内,大就创建在上一层栈帧)

需要十分注意的是:临时变量具有常性!!

问:不能直接返回n1吗?

n1是开辟在栈上的临时变量,出作用域就销毁,数据不由我们控制了

引用返回

:相比传值返回,唯一区别就是临时变量的类型是引用

像代码中写到的,也就是把返回值放到一个int&类型的临时变量,返回这个临时变量(n2 的别名)

int& tmp = n2;
return tmp;

显然危险—— n2已经销毁,可得:

实体返回后仍存在,则可以安全返回它的引用;反之不行

引用返回的优势:

  1. 减少拷贝, 提高效率
  2. 可以对返回值操作

5.2 const 引用

const引用,常引用,引用的实体为常量(具有常性)的引用

要学const引用,首先得知道这一原则:

指针和引用的赋值中 权限可以缩小或平移,不能放大

为什么就是指针和引用?它们都影响实体

int main()
{
	
	int a = 10;//int 可读可写

	
	int& ra = a;//int& 可读可写

	//a ==> ra
	//int ==> int& 
	//可读可写 ==> 可读可写 权限平移,没毛病

    
	const int& cra = a;//const int& 只读

	//a ==> cra
	//int ==> const int& 
	//可读可写 ==> 只读 权限缩小,没毛病


	const int b = 20;//const int 只读
	
	int& rb = b;//int& 可读可写

	//b ==> rb
	//const int ==> int& 
	//只读 ==> 可读可写 权限放大,error

	
	const int& crb = b;//const int& 只读 
	//b ==> crb
	//const int ==> const int&
	//只读 ==> 只读,权限平移,没毛病

	return 0;
}

现在再看一个传值返回的例子:

#include <iostream>

using namespace std;

int f()
{
	int n = 10;
	return n;
}

int main()
{
	//传值返回,产生临时变量
	int& ret = f();//error

	return 0;
}

在这里插入图片描述

要求给int&的初始值必须是可以修改,难道不是吗?

其实

  • 类型的转换(强转/提升/截断)
  • 传值返回

都会产生临时变量

而,最最重要的还是,这个临时变量具有常性,不可修改!

#include <iostream>
using namespace std;

int f()
{
	int n = 10;
	return n;
}

int main()
{
	//类型转换,产生临时变量
	double d = 10;

	//int& rd = (int)d;//error
	const int& rd = (int)d;//ok

	//传值返回,产生临时变量
	//int& ret = f();//error
	const int& ret = f();//ok
	
	return 0;
}

哦哦哦,懂了,然后呢,有什么用?

问:为了减少拷贝提高效率,把函数的参数都设计成引用,好么?

不好,传参会受限制,可能会权限放大——比如形参是可读可写的引用,实参传了只读的实体

那怎么做?

const引用不就来了嘛,形参设计成const引用,你不管传什么,都是 权限缩小或平移

#include <iostream>
using namespace std;

int f(const int& e1)
{
    //...
}

int main()
{
	int a = 10;//缩小
	const int b = 20;//平移

	f(a);
	f(b);

	return 0;
}

5.3 引用的实现

既然它和指针这么像,我们就好好看看它们到底有什么区别,上反汇编…

在这里插入图片描述

lea(load effective address):加载有效地址

mov:类似赋值操作

二者都是

  1. 把 [i] 的有效地址 加载到寄存器 eax 中
  2. 把 eax 中的地址赋给 [ra]/[pa]

可知,引用底层实现上是占空间的,也可知,指针和引用的底层实现是一样的。但是语法上,引用还是不占空间

6. 内联函数

C语言中,对于规模小、结构简单、重复调用的小函数,总是开辟栈帧开辟栈帧,很浪费性能,C++提出内联函数来解决这个问题

6.1 内联 inline

inline是C++中的关键字,只是建议编译器将某函数视为内联函数,编译器会根据情况来决定

特性:

  • 内联函数在预处理后会在调用函数的地方直接展开,不会开辟栈帧,而是直接执行指令

*也代表如果内联函数的规模大/结构复杂,会造成代码膨胀:比如递归,疯狂地展开一堆代码,最直接的体现就是最终出来的exe文件会变得很大

  • 不适合声明定义分离

    • 直接展开,代表内联函数没有函数地址,不会进符号表,链接时会产生链接错误
    // F.h
    #include <iostream>
    using namespace std;
    inline void f(int i);
    // F.cpp
    #include "F.h"
    void f(int i)
    {
    cout << i << endl;
    }
    // main.cpp
    #include "F.h"
    int main()
    {
    f(10);
    return 0;
    }
    // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
    f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
    

应用:小函数

  • 规模小
  • 结构简单
  • 重复调用

本质上,inline是一种空间换时间的做法

面试题

问:宏的优缺点?

优点:

  • 提高代码 复用性
  • 提高性能

缺点:

  • 可读性差,可维护性差,易错
  • 没有类型安全的检查
  • 无法调试

问:如何解决宏的缺点?

  1. 换用const enum来定义常量
  2. inline 小函数

7. auto(C++11)

auto,一种数据类型,可以根据被赋的值自动推导类型,是C++的关键字

为什么会出现这样的数据类型?学到后面,会发现类型的命名简直复杂:

#include <string>
#include <map>
int main()
{
	std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	{
	//....
	}
	return 0;
}

std::map<std::string, std::string>::iterator 是一个类型,也许可以typedef?

typedef char* pstring;
int main()
{
	const pstring p1; 
	const pstring* p2;
	return 0;
}

typedef要求我们 声明变量的时候必须知道之后会给它赋什么类型的值,有时候很苦恼, 那看看auto

std::map<std::string, std::string>::iterator it = m.begin();

auto it = m.begin

舒服

int main()
{
	int x = 1;

	auto a1 = x;//自动识别
	auto a2 = &x;//自动识别
	auto* a3 = &x;//指定auto为指针类型
	auto& a4 = x;//指定auto为引用类型

	//typeid(variable).name():拿到变量类型名称的字符串
	cout << typeid(a1).name() << endl;
	cout << typeid(a2).name() << endl;
	cout << typeid(a3).name() << endl;
	cout << typeid(a4).name() << endl;

	return 0;
}

:int
int *
int *
int //语法上来  说,x是int类型,它的别名找到的一小块内存空间也是int类型

使用注意事项

  1. auto声明引用类型时,必须指定auto为引用类型(否则会直接识别为原类型)
int main()
{
	int x = 1;

	//auto声明/定义指针类型,指不指定都行
	auto a1 = &x;//自动识别
	auto* a2 = &x;//指定auto为指针类型

	//auto声明/定义引用类型,必须指定
	auto a3 = x;//自动识别为int了
	auto& a4 = x;//指定auto为引用类型

	cout << typeid(a1).name() << endl;
	cout << typeid(a2).name() << endl;
	cout << typeid(a3).name() << endl;
	cout << typeid(a4).name() << endl;

	*a1 = 10;
	cout << x << endl;

	a3 = 20;//a3与x无关
	cout << x << endl;

	a4 = 30;
	cout << x << endl;

	return 0;
}
  1. 一行声明多个auto变量时,整行的变量类型必须相同(auto根据第一个变量类型,确定后续的类型)
int main()
{
	auto a1 = 1234, a2 = 123.4;
	return 0;
}

在这里插入图片描述

  1. auto不能作为函数参数

函数开辟栈帧前,要先根据参数计算要开辟多大空间,但是auto大小不确定使得编译器无法计算

void TestAuto(auto a)
{}

在这里插入图片描述

  1. auto不能直接声明数组
int main()
{
	auto a[] = { 1, 2, 3, 4 };
	return 0;
}

在这里插入图片描述

  1. 为了和C++98的auto混淆,C++11的auto只作为类型指示符

8. 基于范围的 for

范围for,自动判断范围,自动迭代

使用时,需要给一个迭代变量,再给定迭代范围,每次迭代都重新创建迭代变量

  • 迭代变量:可以直接用auto,方便
  • 迭代范围:必须是确定的
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	for (auto e : arr)//迭代变量叫啥都可以,e代表element
		cout << e << ' ';
	cout << endl;

	return 0;
}

:1 2 3 4 5 6 7 8 9 10

使用注意事项

  1. 迭代时,编译器每次取范围内的数据,赋值给迭代变量,迭代变量并不改变范围内数据

    int main()
    {
    	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    	for (auto e : arr)
    		e *= 2;
    
    	for (auto e : arr)
    		cout << e << ' ';
    	cout << endl;
    
    	return 0;
    }
    
    :1 2 3 4 5 6 7 8 9 10
    

    如果想改变,设计成 auto& e

    int main()
    {
    	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    	for (auto& e : arr)
    		e *= 2;
    
    	for (auto e : arr)
    		cout << e << ' ';
    	cout << endl;
    
    	return 0;
    }
    
    :2 4 6 8 10 12 14 16 18 20
    

    用auto*可以吗?不行,范围for只是每次取 arr[0]、arr[1],是int类型,不能用int*接收

  2. 迭代范围必须是确定的

    1. 如果迭代数组,范围就是第一个元素到最后一个元素
    2. 如果迭代类,要提供begin( )、 end( ) 两个方法
    //数组传参后变指针,范围不确定了
    void TestRangeFor(int[] arr)
    {
    	for (auto e : arr)
    		cout << e << endl;
    }
    
    int main()
    {
    	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    	TestRangeFor(arr);
    
    	return 0;
    }
    
  3. 迭代的对象要实现++和==的操作。(现在做个了解,以后才讲的请)

9. nullptr

学习了这么长时间,多多少少知道NULL本质上是标识符,但是它在传统C的头文件(stddef.h )中的定义是这样:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

C++中,它的定义出现了bug,被定义成了字面常量0,这也导致许多地方的使用出现问题:

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;
}

:f(int)
f(int)
f(int*)

我们希望NULL是void* 类型的指针空值,但却被识别成字面常量int类型的0,想正常用还要强转,很麻烦。这么简单的bug,C++委员会怎么不修复?

语言要向前兼容,有些代码就按照这个特性跑得好好的,你修复,我代码崩了,我还能好受嘛。所以只能打补丁:

用 nullptr 来代替 NULL 的功能

nullptr 就相当于 (void*)0

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(nullptr);
	return 0;
}

:f(int)
f(int*)

注意:

  • nullptr 是关键字
  • C++11中,sizeof(nullptr) 和 sizeof((void*)0) 相等
  • 后续最好使用 nullptr

今天的分享就到这里,感谢大家能看到这,不足之处多多交流

这里是培根的blog,期待与你共同进步!
For(int[] arr)
{
for (auto e : arr)
cout << e << endl;
}

int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

TestRangeFor(arr);

return 0;

}


3. 迭代的对象要实现++和==的操作。(现在做个了解,以后才讲的请)  



# 9. nullptr

学习了这么长时间,多多少少知道NULL本质上是标识符,但是它在传统C的头文件(stddef.h  )中的定义是这样:

```cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

C++中,它的定义出现了bug,被定义成了字面常量0,这也导致许多地方的使用出现问题:

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;
}

:f(int)
f(int)
f(int*)

我们希望NULL是void* 类型的指针空值,但却被识别成字面常量int类型的0,想正常用还要强转,很麻烦。这么简单的bug,C++委员会怎么不修复?

语言要向前兼容,有些代码就按照这个特性跑得好好的,你修复,我代码崩了,我还能好受嘛。所以只能打补丁:

用 nullptr 来代替 NULL 的功能

nullptr 就相当于 (void*)0

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(nullptr);
	return 0;
}

:f(int)
f(int*)

注意:

  • nullptr 是关键字
  • C++11中,sizeof(nullptr) 和 sizeof((void*)0) 相等
  • 后续最好使用 nullptr

今天的分享就到这里,感谢大家能看到这,不足之处多多交流

这里是培根的blog,期待与你共同进步!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:33:07  更:2022-09-30 00:36:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 5:59:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码