🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻推荐专栏1: 🍔🍟🌯C语言初阶
🐻推荐专栏2: 🍔🍟🌯C语言进阶
🔑个人信条: 🌵知行合一
🍉本篇简介:>:讲解C++中的函数重载、引用、auto关键字、内联函数等.
金句分享:
✨生活本就沉默,但是跑起来有风!✨
前言
本篇文章内容很多,讲解c++入门的一些语法,最好是有C语言的基础,这样学起来更加轻松.内容丰富需要慢慢消化,花费时间也很长,总计约1.5w字,希望能对友友们有所帮助.
目录
- 前言
- 一、函数重载
- 1.1 函数参数类型不同
- 1.2 函数参数的个数不同
- 1.3 函数参数顺序不同
- 1.4 不构成函数重载
- 为什么C不支持函数重载,而C++支持?(重点)
- 1.5 “extern C”
- 二、引用
- 2.1 引用特点:
- 2.2 使用场景:
- 做参数:
- 做返回值:
- 2.3 常引用?
- 2.4 从底层探究引用和指针.
- 三、重新认识一下auto关键字
- 3.1 auto关键字的介绍
- 3.2 使用细节:
- 四、内联函数
- 4.1 观察内联函数的实现:
- 4.2 内联函数的特点
- 4.3 相关面试考点:
- 五、C++的一颗“语法糖”🍭
- 5.1 基于范围的for循环
- 5.2 “糖虽好,注意粘牙哦!”
- 六、认识nullptr
- 七、结语:
一、函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
理论看一遍就行啦,还是直接上栗子好理解吧!
🌰栗子
1.1 函数参数类型不同
//函数参数类型不同
#include <iostream>
#include <stdio.h>
//展开部分常用的
using std::cout;
using std::cin;
using std::endl;
namespace cjn
{
//函数1
int add(int e1, int e2)
{
cout << "整形:";
return e1 + e2;
}
//函数2
double add(double e1, double e2)
{
cout << "浮点形:";
return e1 + e2;
}
}
int main()
{
int a = 0, b = 0;
double c = 0, d = 0;
cin >> a >> b;
//a和b是int型,会调用int add(int e1, int e2)函数
cout << cjn::add(a, b) << endl;
cin >> c >> d;
//c和d是double型,会调用double add(double e1, double e2)
cout << cjn::add(c, d) << endl;
return 0;
}
输入:
2 3
3.4 5.2
输出
整形:5
浮点形:8.6
函数1和函数2虽然函数名相同,但是函数的参数不同,构成函数重载.
1.2 函数参数的个数不同
🌰栗子
//函数参数个数不同时
#include <iostream>
#include <stdio.h>
using std::cout;
using std::cin;
using std::endl;
namespace cjn
{
void fun() //函数1
{
cout << "fun()" << endl;
}
void fun(int a) //函数2
{
cout << "fun(int a)" << endl;
}
}
int main()
{
cjn::fun(); //会调用函数1
cjn::fun(0); //会调用函数2
return 0;
}
运行结果:
fun()
fun(int a)
此时还有一个特殊情况:
namespace cjn
{
void fun() //函数1
{
cout << "fun()" << endl;
}
void fun(int a=4) //函数2
{
cout << "fun(int a)" << endl;
}
}
int main()
{
cjn::fun();//此时编译器不知道应该调用哪一个函数
return 0;
}
分析:
由于函数2设置了缺省值,所以在不传参时,会产生混乱.
1.3 函数参数顺序不同
🌰栗子
//函数参数顺序不同
#include <iostream>
#include <stdio.h>
using std::cout;
using std::cin;
using std::endl;
namespace cjn
{
void fun(int a,char b) //函数1
{
cout << "fun(int a,char b)" << endl;
}
void fun(char a,int b) //函数2
{
cout << "fun(char a,int b)" << endl;
}
}
int main()
{
cjn::fun(1,'c'); //会调用函数1
cjn::fun('a',0); //会调用函数2
return 0;
}
运行结果:
fun(int a,char b)
fun(char a,int b)
1.4 不构成函数重载
函数的返回值不同 :不构成函数重载
//不支持函数重载
namespace cjn
{
int fun(char a,int b) //函数1
{
cout << "fun(int a,char b)" << endl;
}
double fun(char a,int b) //函数2
{
cout << "fun(char a,int b)" << endl;
}
}
为什么C不支持函数重载,而C++支持?(重点)
上面也说了,返回值不同也不支持函数重载,让我们从底层来揭秘吧!
在C/C++
中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。这些在C语言中的程序环境阶段有细讲.
我们知道,如果我们只有函数的声明,相当于只拿到了承诺,具体的函数定义并没有拿到,要在最后的链接阶段去通过符号表(很重要)拿到函数地址(兑现承诺).
而符号表中对于函数名的修饰规则在C和C++中是不同的.
例如:
//以下列函数为例
int Add(int a, double b)
{
......
}
void Swap(double* e1, double* e2)
{
double tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下图是g++修饰后的名字规则。
C语言和C++的函数名修饰的不同
在C语言中,只是简单的将函数名直接存入符号表,而C++则考虑到函数的参数类型,函数名长度等因素.
而g++环境下的C++中的函数修饰后变成【_Z+函数长度+函数名+类型首字母】
疑问:如果两个函数函数名和参数是一样的,返回值不同构成函数重载吗?
示例:
int add(int a, int b)
{
;
}
double add(int a, int b)
{
;
}
int main()
{
add(2, 3);//调用哪个函数?返回值没有体现也无法体现在调用处
return 0;
}
答案是:不构成函数重载,即使修改了底层函数名修饰规则也不行,因为在调用函数时,返回值无法体现,无法区分该调用哪个函数.
1.5 “extern C”
由于C和C++编译器对函数名字修饰规则的不同,在有些场景下可能就会出问题,比如:
情况1:
C++中调用C语言实现的静态库或者动态库,反之亦然
情况2:
多人协同开发时,有些人擅长用C语言,有些人擅长用C++
在这种混合模式下开发,由于C和C++编译器对函数名字修饰规则不同,可能就会导致链接失败,在该种场景下,就需要使用extern “C”。在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。
🌰栗子:
为了能在C/C++工程中都能使用,函数声明时需加上extern “C”
#ifdef __cplusplus//如果是c++项目(这里要求用c语言实现)
extern "C"//此时这条语句会执行
{
#endif
int Add(int left, int right);
int Sub(int left, int right);
#ifdef __cplusplus
}
#endif
解释:
#ifdef _cplusplus
}
#endif
作用:(1):c++工程中保证是以C的方式实现
_如果是C++工程,编译器已经定义_cplusplus宏,编译时该宏是可以被识别的,被声明的函数就被extern "C"修饰了,此时C++编译就知道,该函数是按照C的方式编译的,这样在链接时就会按照C的方式找函数名字.
(2):C工程中不受影响
如果是C工程,编译器未定义_cplusplus宏,编译时该宏无法被是被,则条件编译就无效,函数就不会被extern "C"修饰 .
二、引用
引入:
C指针玩法:
在C语言阶段,我们可以通过一个指针去找变量,指针就好比是老板的秘书(一级指针),老板太忙了,只给秘书(一级指针)留了联系方式,我们可以通过秘书(一级指针)去找变量,如果秘书(一级指针)也比较忙,就会给秘书也会自己的秘书留下联系方式,即秘书的秘书(二级指针).
C++引用玩法:
例如:有个小女孩真名叫“涂山苏苏”,我们也可以叫她“小蠢货”,”蠢货苏”等别名,或者“苏苏”等其它小名.这些名词虽然不一样,但是代表的内容都是“涂山苏苏”本人,这便是C++引用的做法,取别名.
图解:
示例代码:
# include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main()
{
int a = 5;
//下面都是对a的引用,即a的别名.
int& b = a;
int& c = a;
int& d = a;
int& e = a;
cout << a << " " << b << " " << c << " " << d << " " << e << endl;
return 0;
}
运行结果:
5 5 5 5 5
2.1 引用特点:
- 引用必须初始化.
int main()
{
int a = 5;
//如果我们引用不初始化
int& b;
return 0;
}
改代码会显示错误信息:
这也很好理解,引用就类似与取别名,如果连对象都没有,那别名还有啥意义.
- 引用一旦确定了引用实体后,就不能像指针一样改变指向了.
int main()
{
int a = 5;
int c = 1;
int& b=a;
b = c;//这样b就是c的别名了吗?
cout << a << " " << b << " " << c << endl;
return 0;
}
运行结果:
1 1 1
b=c
并不是将b
改为c
的别名,而是赋值,将b
的值给改了,b
改了,那也就等于a
也改了.
2.2 使用场景:
做参数:
引用和指针在简单传参(后续有复杂的自定义对象传参,效果更加明显)是的对比.
做返回值:
(1)我们之前的一般返回是这样的.
//普通返回
int test()
{
static int a = 0;
a += 5;
return a;
}
int main()
{
int& c = test();
cout << c << endl;
return 0;
}
(2)改用引用作为返回值后:
//引用返回
int& test()//注意看返回值的类型
{
static int a = 0;
a += 5;
return a;//返回的是a的别名
}
int main()
{
int& c = test();//此时c是作为别名接收返回值
cout << c << endl;
return 0;
}
图解:
引用作为返回值的写法减少了拷贝,所以明显效率更高.
那以后我们都用引用作为返回值吗?(友情提示引用虽好,可不要贪杯哦!);
我们看一下下面的情况:
//引用返回
int& test()
{
int a = 0;
a += 5;
return a;
}
int add(int a, int b)
{
int sum = a + b;
return sum;
}
int main()
{
int& c = test();
add(22, 44);
cout << c << endl;
return 0;
}
结果:
66
c
是函数test()
的返回值,我们打印的是c
,为啥结果确实add(22,44)
的返回值,是巧合吗?
这就要考虑所在环境了,还是画图比较好理解,上图解!
如果引用做返回值时,返回的空间是被系统收回的,那就很危险.
引用作为返回值的时候,可以修改返回值,或者获取返回值,而不是获取返回值的拷贝(临时变量).
(1)普通返回:
//普通返回
int test()
{
static int a = 0;
a += 5;
return a;
}
int main()
{
int c = test();
c = 95;//对c不会影响a,因为它只是a的拷贝返回
cout << test() << endl;//调用了两次,所以结果为10
return 0;
}
运行结果:
10
(2)引用返回:
//引用返回
int& test()
{
static int a = 0;
a += 5;
return a;
}
int add(int a, int b)
{
int sum = a + b;
return a + b;
}
int main()
{
int& c = test();
c = 95;//对c修改,就是对a修改.
cout << test() << endl;
return 0;
}
运行结果:100
可以通过调试发现,test返回的是a的别名,所以c是a的别名的别名.
小结:
1.引用做参数基本上所有场景都可以.
(1)方便做输出型参数(例如:swap()函数),可以用传引用改变实参.
(2)传参是以别名的形式,中间没有拷贝,可以提高效率.
2.引用做返回值时需要特别注意,如果出了作用域,对象空间被系统收回了,就不能用引用返回.其它情况建议用引用返回,可以减少拷贝,提高效率.
(1)同样,可以减少拷贝,提高效率.
(2)可以修改返回值,或者是获取返回值.(后续会遇到这种情况).
2.3 常引用?
(1)权限放大:
从只读–>可读,可写,权限放大会报错.因为不安全.
//情况1
const int a = 6;
//错误写法
int& ra = a; //该语句编译时会报错,因为a变量具有常性,而ra是可读可写,权限不能放大.
//正确写法:
const int& ra = a;//权限的平移
//情况2
//错误写法
int& b = 5; // 该语句编译时会出错,b为常量
//正确写法
const int& b = 5;//权限的平移
//情况3
double d = 13.14;
//错误写法
int& rd = d; // 这里会发生隐式类型转换,而产生的临时变量具有常性.
//正确写法
const int& rd = d;
//情况4
int Test_Const()
{
int x = 2, y = 3;
int sum = x + y;
return sum;
}
int main()
{
//错误写法
int& ret=Test_Const();//返回值是临时变量(因为函数栈帧被销毁了,需要借助寄存器,或者别的产生临时变量返回)的拷贝,临时变量具有常性.
//正确写法
const int& ret = Test_Const();
return 0;
}
情况3的特别说明:隐式转换(操作数两边数据类型不同时,要保证数据两边的类型不变,需要借助临时变量).
(2)权限缩小:
从可读可写–>只读,权限缩小,更加安全.
int& Test_Const()
{
int x = 2, y = 3;
int sum = x + y;
return sum;
}
int main()
{
//权限平移
int& ret = Test_Const();//注意Test_Const函数的返回值是sum的别名,并不是具有常性的临时变量,所以这里不会报错
//缩小
const int& ret = Test_Const();//从可读可写-->只读,权限缩小,更加安全,不会报错.
return 0;
}
小结:
权限可以平移和缩小,但是不可以放大,使用时需要注意.
2.4 从底层探究引用和指针.
示例代码:
int main()
{
int a = 4;
//从语法上看,引用不开空间,而是用别名直接对a操作
int& b = a;
b = 5;
//从语法看,指针需要开空间存储a的地址,然后通过地址去找a
int* p = &a;
*p=20;
return 0;
}
我们通过调试窗口,打开反汇编窗口,观察汇编代码:
我们发现,在底层引用和指针的实现逻辑是一样的.都需要开空间,但是在语法上,我们依旧认为它是没有开空间的.
就好比:
老婆饼里面没有老婆,红烧狮子头里面没有狮子,娃娃菜里面没有娃娃.
- 引用语法概念上定义一个变量的别名,而指针是存储一个变量地址。
- 引用在定义时必须初始化,否则编译器不知道是谁的“别名”,而指针没有规定,只不过我们习惯性初始化为NULL而已.
- 引用在初始化时引用一个实体后,就不能再引用其他实体(上面有讲到),而指针可以在任何时候指向任何一个同类型实体,并不受限制.
- 没有
NULL
引用,但有NULL
指针 - 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
4个字节) - 对引用的赋值,就是对引用实体进行修改,而指针+1或者赋值则是对实体的地址编号操作,并不会影响实体.
- 不存在多级引用,但是可以多个引用可以引用一个实体,即一个实体有多个别名.指针可以有多级指针.
- 访问实体方式不同,指针需要显式解引用,引用编译器会自己转换处理.
- 安全性:引用比指针使用起来相对更安全 ,好歹不存在空指针吧!😂😂😂
三、重新认识一下auto关键字
3.1 auto关键字的介绍
在c语言中:
auto是C语言的一个关键字,关键字主要用于声明变量的生存期为自动,这个关键字不怎么多写,因为所有的局部变量默认就是auto的。
int a=0;//默认就是自动生存期
//等价于下面的
auto int a=0;//写成这样也太麻烦了,我们一般直接省略不写.
C语言中提供了存储auto,register,extern,static说明的四种存储类别。四种存储类别说明符有两种存储期:自动存储期和静态存储期。
自动存储期:
auto和register。自动变量指在局部创建该变量,然后出了局部的作用域,该变量的声明周期就结束了,还给操作系统了.
静态存储周期:
extern,static
C++赋予了auto新的“生命”:
C++11标准中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型
指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得.
🌰栗子
#include <stdlib.h>
# include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main()
{
int a = 3;
double b = 4.1;
auto c = a + b;//会自动推导出c的类型
cout << c << endl;
cout << typeid(c).name() << endl;//typeid(c).name()会打印变量c的类型,后续会介绍,这里了解一下就行.
return 0;
}
这样看似乎auto关键字的作用不是很大,我们可以直接写double c,那是因为没有遇到复杂的场景,试着看一下下面这段代码(我们暂时不需要看懂代码的作用,只需要关注类型名即可).
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{
{ "name", "名字" },
{ "age", "年龄" },
{"sex","性别"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
由于在工程中很多情况下,我们不能展开头文件,难免会遇到这样的类型:(上面代码的it的类型)
std::map<std::string, std::string>::iterator
使用宏替换可以吗?
可以是可以,但是宏替换的缺点你是否能接受?
- 没有安全检查,直接进行替换.
- 可读性很差,也不方便调试.
那是否有小伙伴想到使用typedef进行类型重定义呢?
其实typedef也是有缺点的.
例如:
typedef char* pchar;
int main()
{
char arr[] = "cjn";
//const pchar p1;//此语句报错,这里是指向不可改变的常量指针,定义时需要初始化
const pchar p1=arr;
*p1 = 'x';//指向不可以改变,但指向的内容可以.
printf("%s\n", arr);
char* parr =arr;//定义一个字符指针
const pchar* p2=&parr;//指向字符指针的地址
**p2 ='a';//解引用后的指针与p1类型一样,指向的内容可以改
//*p2 = NULL;//但是指针指向不能改
printf("%s\n", arr);
return 0;
}
const pchar等价于char* const表示指针的指向 不能改变.
因为此处类型pchar
相当于是类型char*
的别名,const pchar
表示const修饰的是char*
,即char*指针的的指向不能被修改,但是其指向的内容可以修改.
const pchar*的类型等价于 char * const*
.
typedef有的场景很容易让我们误解类型.
3.2 使用细节:
- 使用时必须初始化:
int main()
{
auto a;
return 0;
}
原因:
在编译阶段编译器需要根据初始化表达式来推导auto的实际类型,而未初始化变量,则无法进行推导。注意auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
-
不能直接推导出引用类型.
int main() { int a = 3; auto b = a;//如果想b是a的引用,这里并不能达到我们想要的效果. //正确写法 auto& c = a;//此时c就表示是一个引用类型,auto可以推导出是int类型 return 0; }
-
如果是推导指针类型:
此时,auto和auto*没有区别(记住,只有在推导指针类型时没有区别,试着理解一下,因为指针类型可以直接推导出来)
int main() { int a = 3; auto b = &a;//这里auto会自动推导成int*,表示b是一个整形指针int*指向a auto* c = &a;//此处auto会推导称为int与*结合为int* //所以说此处auto和auto*声明变量没区别. return 0; }
-
同一行定义多个变量的情况:
默认将第一个推导出来的类型作为整条语句其他变量的类型.
int main() { auto x = 3, y = 5; //同类型不会报错 auto a = 3, b = 4.5, c = 3; //不同类型会报错,因为会将推导出来的第一个类型作为这条语句所有变量的类型 return 0; }
-
不能用来声明数组:
int main()
{
auto x[5] = { 1,2,3,4,5 };//报错
return 0;
}
6.不能做形参推导:
void test(auto a)//报错
{
......
}
四、内联函数
概念:
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧
的开销,内联函数提升程序运行的效率 .
//默认Debug下,内联不会起作用.
inline int add(int a, int b)
{
return a + b;
}
int main()
{
int c = add(2, 3);
return 0;
}
需要修改默认属性,更加方便我们在debug版本下观察内联函数.
视频教程:
4.1 观察内联函数的实现:
4.2 内联函数的特点
-
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替
换函数调用.缺陷:由于是在调用处展开,则代码量将会扩大,也就导致目标文件的增大.
优势:少了调用开辟函数栈帧的开销,提高程序运行效率。
-
你说内联就内联?编译器是不会信任你的,你将编译器搞崩溃了(代码膨胀咋办?
所以inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同**,一般建议:将函数规
模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、**不是递归、频繁调用的函数
采用inline修饰,否则编译器将不会采用=内联方式.
所以不要以外任何情况下内联都是好的,要视情况而定,对于短小的函数,且大量频繁调用的采用内联比较合适.
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会
找不到 .
//test.c(主函数区)
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
#include "add.h"
int main()
{
int a = 2, b = 3;
cout << add(a, b) <<endl;
}
//add.c
int add(int a, int b)
{
return a + b;
}
//add.h
inline int add(int a, int b);
编译器可以编译通过,但是链接不上,因为声明时显示采用内联方式,内联函数的函数名并不会进符号表(因为会展开),而在主函数区中执行add(a, b)
时,就找不到函数的地址,因为符号表中找不到.
4.3 相关面试考点:
(1) 宏函数的优缺点?
优点:
1.增强代码的复用性。(在预处理阶段直接进行宏替换)
2.提高性能。(没call指令,不需要为函数开辟栈帧)
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。(标识符不具备指向性)
3.没有类型安全的检查 。(编译器不会报错)
(2) **C++**有哪些技术替代宏?
- 常量定义 换用const enum
- 短小函数定义 换用内联函数
五、C++的一颗“语法糖”🍭
5.1 基于范围的for循环
在c++11中,有一种写法是基于范围的for循环,被称为“语法糖”,让我们尝一尝这颗“糖”甜不甜吧?🍭🍭🍭
以前我们打印数组中的每个元素,我们是这样的.
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//对数组的每个元素*2
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
arr[i] *= 2;
}
//打印每个元素
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
cout << arr[i] << " ";
}
return 0;
}
使用“语法糖”后:
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//对数组的每个元素*2
错误写法
//for (auto x:arr)
//{
// x *= 2;
//}
//正确写法
for (auto& x:arr)
{
x *= 2;
}
//打印每个元素
for (auto x : arr)
{
cout << x << " ";
}
return 0;
}
解释:
对于一个有范围的集合而言,程序员来手动再写一遍范围是没有必要的,多余之举,有时候还会写错(例如:忘记下标从0开始)。此事交给任劳任怨的编译器完成比较好,因此C++11标准中引入了基于范围的for循环。
格式:
for循环后的括号由冒号(😃 分为两部分:
第一部分:范围内用于迭代的变量(取名随意,与给变量名起名一样,有意义即可),
第二部分:表示被迭代的范围,数组名表示范围是整个数组.
5.2 “糖虽好,注意粘牙哦!”
for循环迭代的范围必须是确定的:
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void print_for(int array[])//数组传参过来之后就是首元素的地址,而不是整个数组,所以范围不确定
{
for (auto& e : array)
cout << e << endl;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
print_for(arr);
return 0;
}
六、认识nullptr
在C语言阶段,我们提倡创建一个变量之后,要给定一个初始值,减少一些可能出现的未知错误(例如:野指针,随机值等),对于指针我们经常使用下面这段代码进行初始化.也就是NULL
.
#include <stdio.h>
int main()
{
int* p = NULL;
return 0;
}
那NULL
究竟是什么呢?我们右击NULL
,在弹出的快捷菜单中,选择“转到定义”命令,可以查看到下面这段解释.
很明显,NULL
就是个宏定义,而这里NULL
可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
不论采取上面何种定义,在使用空值(NULL
)的指针时,都不可避免的会遇到一些麻烦 .
栗子:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);//这里想调用f(int*)函数
f((int*)NULL);
return 0;
}
运行结果:
f(int)
f(int)
f(int*)
这里因为NULL
被定义为0,所以并没有调用f(int*)
函数成功.因为语言必须向下兼容,所以不能直接修改NULL
的定义,为了解决这一问题,C++11中引入了新的的nullptr为void*类型.
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下
将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的**。**
- 在**C++11中,sizeof(nullptr) **与sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
七、结语:
C++的入门知识就分享到这里了,下次会分享"类和对象的知识",那时候应该要等到暑假啦,码文不易,如果觉得文章有帮助的话,可以三连支持一波吗?💗💗💗
欢迎友友们私信与牛牛讨论问题.,只是牛牛的认知范围有限,目前只关注c语言,数据结构,C++等部分领域.