网站定制怎么收费,网络营销的实现方式有哪些,零基础学it哪个专业好,网站建设服务中企动力#x1f47b;内容专栏#xff1a; C/C编程 #x1f428;本文概括#xff1a; C入门学习必备语法 #x1f43c;本文作者#xff1a; 阿四啊 #x1f438;发布时间#xff1a;2023.9.3 前言 C是在C的基础之上#xff0c;容纳进去了面向对象编程思想#xff0c;并增加… 内容专栏 C/C编程 本文概括 C入门学习必备语法 本文作者 阿四啊 发布时间2023.9.3 前言 C是在C的基础之上容纳进去了面向对象编程思想并增加了许多有用的库以及编程范式 等。熟悉C语言之后对C学习有一定的帮助本章节主要目标 补充C语言语法的不足以及C是如何对C语言设计不合理的地方进行优化的比如作用 域方面、IO方面、函数方面、指针方面、宏方面等。为后续类和对象学习打基础 C关键字(C98) C总计63个关键字C语言32个关键字 ps:这里仅列出C的关键字混个眼熟就行这里不作细致讲解后面每每用到会做讲解。 asmdoifreturntrycontinueautodoubleinlineshorttypedefforbooldynamic_castintsignedtypeidpublicbreakelselongsizeoftypenamethrowcaseenummutablestaticunionwchar_tcatchexplicitnamespacestatic_castunsigneddefaultcharexportnewstructusingfriendclassexternoperatorswitchvirtualregisterconstfalseprivatetemplatevoidtrueconst_castfloatprotectedthisvolatilewhiledeletegotoreinterpret_cast
命名空间 在C/C中变量、函数和后面要学到的类都是大量存在的这些变量、函数和类的名称将都存在于全局作用域中可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的。 #include stdio.h
#include stdlib.hint rand;
int main()
{printf(%d, rand);return 0;
}编译后后报错error C2365: “rand”: 重定义以前的定义是“函数” 在C语言中我们会写出这样的代码我们想命名一个与stdlib库中函数一样的名字rand 但是C语言没办法解决类似这样的命名冲突问题所以C提出了namespace来解决。
命名空间的定义
1.定义 定义命名空间需要使用到namespace关键字后面跟命名空间的名字然后接一对{ }即可{ }中即为命名空间的成员。 //Asi是命名空间的名字
//命名空间定义
namespace Asi
{// 命名空间中可以定义变量/函数/自定义类型int rand 10;int Add(int a, int b){return a b;}struct Node{struct Node* next;int val;};
}2.命名空间的嵌套
namespace Asi
{// 命名空间中可以定义变量/函数/自定义类型int rand 10;int Add(int a, int b){return a b;}struct Node{struct Node* next;int val;};namespace _Asi{// 命名空间中可以定义变量/函数/自定义类型int rand 20;int Add(int a, int b){return a b;}struct Node{struct Node* next;int val;};}
}int main()
{printf(%d\n, Asi::rand);printf(%d\n, Asi::_Asi::rand);
}打印结果 1020命名空间的使用
1.加命名空间名称及作用域限定符
namespace Asi
{// 命名空间中可以定义变量/函数/自定义类型int i, j;int rand 10;int Add(int a, int b){return a b;}struct Node{struct Node* next;int val;};
}
int main()
{//错误写法//printf(%d\n,i);//error:“i”未声明的标识符//printf(%d\n,Add(0, 1));// “Add”: 找不到标识符 编译器默认会到全局作用域中去找//Asi::struct Node node;//正确写法printf(%p\n,Asi::rand); // “::”符号为作用域限定符printf(%d\n,Asi::Add(0, 1));struct Asi::Node node;return 0;
}命名空间使用的其他两种写法
2.展开命名空间(全部授权) 使用using namespace命名空间名称 引入
//展开命名空间
using namespce Asi;
int main()
{printf(%d\n, N::i);printf(%d\n, j);Add(10, 20);return 0;
}⚠️注意展开命名空间是一件非常有风险的做法观察以下代码发现再去编译这段代码编译报错“rand”不明确的符号 因为编译器不知道这个rand是命名空间里面的还是库函数里面的。
//展开命名空间
using namespce Asi;
int main()
{printf(%p\n,rand);printf(%d\n, i);printf(%d\n, j);Add(10, 20);return 0;
}针对这种情况我们可以使用第三种用法。
3.展开某一个变量/函数/自定义类型(部分授权) 使用 using 命名空间中某个成员 引入
using Asi::Add;
using Asi::i;
int main()
{printf(%p\n, rand);printf(%d\n, Add(1, 2));printf(%d\n, i);return 0;
}C输入与输出
using namespace std是干嘛的 以下我们这一串代码我们会在C中常用那么std是什么std其实是C标准库的命名空间也就是说C标准库中所有用到的东西都会放到std当中。那么有同学会问那头文件#include iostream有何用处呢头文件其实被封装到std命名空间里面引用头文件会将头文件中的内容展开到std命名空间中注意这里说的意思是头文件的展开是将头文件中的内容拷贝到std命名空间。 命名空间的展开就是将#include iostream中的所有内容进行授权。使用std和endl编译器就会到std命名空间当中去找所以如果没有写using namespace std编译器就会报出“cout”“endl”未声明的标识符
#include iostream
using namespace std;
int main()
{cout hello world endl;return 0;
}ps 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时必须包含 iostream 头文件以及按命名空间使用方法使用std。cout和cin是全局的流对象endl是特殊的C符号表示换行输出他们都包含在包含iostream 头文件中。是流插入运算符是流提取运算符。使用C输入输出更方便不需要像printf/scanf输入输出时那样需要手动控制格式。C的输入输出可以自动识别变量类型。实际上cout和cin分别是ostream和istream类型的对象和也涉及运算符重载等知识这些知识我们我们后续才会学习所以我们这里只是简单学习他们的使用。后面我们还有一个章节更深入的学习IO流用法及原理。 ⚠️注意早期标准库将所有功能在全局域中实现声明在.h后缀的头文件中使用时只需包含对应头文件即可后来将其实现在std命名空间下为了和C头文件区分也为了正确使用命名空间规定C头文件不带.h旧编译器(vc 6.0)中还支持iostream.h格式后续编译器已不支持因此推荐使用iostream std的方式。 #include iostream
using namespace std;
int main()
{int a;double b;char c;// 可以自动识别变量的类型cina;cinbc;coutaendl;coutb cendl;return 0;
}
// ps关于cout和cin还有很多更复杂的用法比如控制浮点数输出精度控制整形输出进制格式等等。
//因为C兼容C语言的用法指定输出格式时可以暂时用scanf和printf的用法。缺省参数
缺省参数概念 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参。 void func(int a 10)
{cout a endl;
}
int main()
{func();//没有传参使用缺省参数func(20);//给定参数使用指定的参数return 0;
}打印 10 20 缺省参数分类
全缺省参数
void Func(int a 10, int b 20, int c 30)
{couta aendl;coutb bendl;coutc cendl;
}半缺省参数
void Func(int a, int b 10, int c 20)
{couta aendl;coutb bendl;coutc cendl;
}⚠️注意 1. 半缺省参数必须从右往左依次来给定不能间隔给定 2. 缺省参数不能在函数声明和定义中同时出现只用在声明当中给定定义不给定。 3. 缺省值必须是常量或者全局变量 4. C语言不支持编译器不支持) 函数重载 我们当时在学C语言的时候对于int类型我们需要写一个Swapi函数对于double类型我们需要写一个Swapd函数但是在C中有一个同名函数的概念对于所有类型的交换函数我们可以只用写一个Swap重载函数就行。 函数重载概念
函数重载是函数的一种特殊情况C允许在同一作用域中声明几个功能类似的同名函数这些同名函数的形参列表(参数个数 或 参数类型 或 参数类型的顺序)不同常用来处理实现功能类似数据类型不同的问题。
#includeiostream
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{cout int Add(int left, int right) endl;return left right;
}
double Add(double left, double right)
{cout double Add(double left, double right) endl;return left right;
}
// 2、参数个数不同
void fun()
{cout fun() endl;
}
void fun(int a)
{cout fun(int a) endl;
}
// 3、参数类型顺序不同
void fun(int a, char b)
{cout fun(int a,char b) endl;
}
void fun(char b, int a)
{cout fun(char b, int a) endl;
}
int main()
{Add(10, 20);Add(10.1, 20.2);fun();fun(10);fun(10, a);fun(a, 10);return 0;
}那么函数重载是怎么实现的呢
C支持函数重载的原理——名字修饰(name Mangling)
为什么C支持重载而C语言不支持重载? 这其实与编译器的编译有关让我们回顾一下C语言时期学习的编译链接相关知识(对这个概念熟悉的同学可以直接跳到符号表)
预处理-编译-汇编-链接 下面我们可以在gcc平台用几条命令回顾一下程序编译的几个阶段我们在gcc环境下简单写一份.c文件 1.预处理 选项: gcc -E test.c -o test.i 预处理完成之后就停下来预处理之后产生的结果都放在test.i文件中。 2.编译 选项: gcc -S test.c 编译完成之后就停下来结果保存在test.s中 3.汇编 选项gcc -c test.c 汇编完成之后就停下来结果保存在test.o中
符号表
好了程序编译大概分为这么几个阶段下面着重理解一下符号表的概念什么是符号表 符号表内包含了变量、函数、类、结构体等等标识符记录了各种变量、函数的名字与其地址一一映射的关系 在C语言中编译器其实就是直接以你自己写的函数名本身充当符号表中的函数名所以在C语言中不支持函数重载 那么C是如何支持函数重载的呢 这里每个编译器都有自己的函数名修饰规则
函数名修饰规则
由于Windows下vs的修饰规则过于复杂而Linux下g的修饰规则简单易懂下面我们先在Linux环境下进行演示。
Linux环境下函数名修饰规则
通过下面我们可以看出gcc的函数修饰后名字不变。而g的函数修饰后变成_Z 函数长度 函数名 类型首字母objdump -S 文件名会对给定的目标文件或可执行文件进行反汇编并显示汇编代码以及与源代码对应的内容。 采用C语言编译器编译后的结果 结论在linux下采用gcc编译完成后函数名字的修饰没有发生改变。采用C编译器编译后结果 所以在C中类型不同因参数个数、参数类型的顺序、参数类型本身的不同就会构成函数自身的函数名字所以汇编指令 call (调用)函数时调用函数其实根据函数名在符号表中去寻找对应的地址所以容易误认为的函数返回值并不属于函数重载的特性。 结论 在linux下采用g编译完成后函数名字的修饰发生改变编译器将函数参数类型信息添加到修改后的名字中。
Windows下函数名修饰规则
#include iostream
using namespace std;void func1(int i, double d);
//{
// cout void func(int a, double b) endl;
//}
void func2(double d, int i);
//{
// cout void func(double a, int b) endl;
//}//函数名修饰规则
int main()
{func1(1, 1.1); // call _Z5func11idfunc2(1.1, 1); // call _Z5func2di
}将代码编译 修饰后名字由 “”开头接着是函数名由“”符号结尾的函数名后面跟着由“”结尾的类名“C”和名称空间“N”再一个“”表示函数的名称空间结束第一个“A”表示函数调用类型为“__cdecl”接着是函数的参数类型及返回值由“”结束最后由“Z”结尾。 对比Linux会发现windowws下vs编译器对函数名字修饰规则相对复杂难懂但道理都是类似的我们就不做细致研究了。
通过这里就理解了C语言没办法支持重载因为同名函数没办法区分。而C是通过函数修饰规则来区分只要参数不同修饰出来的名字就不一样就支持了重载。如果两个函数函数名和参数是一样的返回值不同是不构成重载的因为调用时编译器没办法区分。
引用
引用概念 引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。 类型 引用变量名(对象名) 引用实体
void TestRef()
{int a 10;int b a;//定义引用类型//取地址cout a endl;cout b endl;
}注意1. 引用类型必须和引用实体是同种类型的 2.在C语言阶段我们都知道是取地址操作符但与这里的引用并不冲突取决于他们各自的使用方式。 引用特性
一个变量可以有多个引用引用在定义时必须初始化引用一旦引用一个实体再不能引用其他实体
void TestRef()
{int a 10;// int ra; // 该条语句编译时会出错因为没有初始化引用int ra a;int rra a;printf(%p %p %p\n, a, ra, rra);
}使用场景
1、引用做函数参数 在学数据结构时我们对链表的PushBack()函数进行传参需要传入结构体一级指针用二级指针来接收。
typedef struct ListNode
{int val;struct ListNode* next;
}ListNode;//C语言一级指针传参二级指针用来接收
void PushBack(ListNode** pphead,int x)
{ListNode* newnode (ListNode*)malloc(sizeof(ListNode));if (*pphead NULL){*pphead newnode;}else{;}
}
int main()
{ListNode* plist NULL;PushBack(plist, 1);PushBack(plist, 2);PushBack(plist, 3);
}而使用C的引用传递参数在这里方便了许多也变得容易理解。
typedef struct ListNode
{int val;struct ListNode* next;
}ListNode,* PListNode;//用C引用参数接收
//void PushBack(PListNode pphead, int x)
void PushBack(ListNode* pphead, int x)
{ListNode* newnode (ListNode*)malloc(sizeof(ListNode));if (pphead NULL){pphead newnode;}else{;}
}
int main()
{ListNode* plist NULL;PushBack(plist, 1);PushBack(plist, 2);PushBack(plist, 3);
}2.引用做返回值 在C语言中我们学过函数传值返回的概念编译器在给Count()函数返回值做返回的时候为了确保出了作用域n变量会被销毁会生成一个临时变量(寄存器或者一块栈帧空间)然后再返回给ret 而在C中可以用引用返回观察下面的代码Count()函数返回的是n的别名那么大家可能会想n出了作用域不就被销毁了吗ret再去拿到别名不也就没了吗这里想说的是空间地址还在只是数据资源被清理了。那么存放到ret的结果可能是1也可能是随机值这就取决于编译器环境、Count函数栈帧空间是否会立马释放置为随机值。 但是将ret改为引用类型接收打印两个ret的值第一个为1第二个为随机值为什么呢这里首先需要说明cout ret endl 利用了运算符重载函数(具体后面章节会细讲)。 第一个cout ret endl 首先ret会传参给运算符重载函数那么打印的ret就是正常值1然后开辟了运算符重载函数栈帧于是就将原来的Count()函数栈帧空间给覆盖了那么此时再去取ret也就是n的别名n已经被置为随机值了那么打印的ret也就是一个随机值。 这里如果理解了的话大家可以看看下面这个代码的结果是什么
#include iostream
using namespace std;
//注该代码仅用于理解引用底层的逻辑不建议学习此代码。
int Add(int a, int b)
{int c a b;return c;
}
int main()
{int ret Add(1, 2);Add(3, 4);cout ret ret endl;return 0;
}打印上述代码结果 ret 7 解释 那么引用作为返回值到底怎么使用呢 ⚠️注意如果函数返回时出了函数作用域如果返回对象还在(还没还给系统)则可以使用引用返回如果已经还给系统了则必须使用传值返回。
值和引用作为返回值类型的性能比较
#include time.h
struct A{ int a[10000]; };
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;
}打印上述代码结果 TestFunc1 time:162 TestFunc2 time:2 通过比较发现传值和指针在作为传参以及返回值类型上效率相差很大。
传引用传参和传引用返回的作用 传引用传参(任何时候都可以使用) 提高效率输出型参数(形参的修改影响实参) 传引用返回(出了作用域对象还在才能使用) 提高效率修改返回对象(比如说C语言实现的顺序表需要写SLFind()和SLModify()两个接口函数而CPP利用传引用返回只需要写一个SLAt()接口可以调用函数后进行修改。) 常引用 在引用的过程中权限可以平移可以缩小但是不能放大。 void TestConstRef()
{const int a 10;//权限的放大//int b a; //注意int b a;是可以的这里是赋值拷贝因为b的 修改不影响a//权限的平移const int c a;//权限的缩小int x 20;const int y x;//error:这里并不是类型不匹配整型传递给double类型中间会发生隐式类型转换//产生一个临时变量而临时变量具有常属性。int i 30;//double d i; const double d i; }引用和指针的区别 在语法概念上引用就是一个别名没有独立空间和其引用实体共用同一块空间。 int main()
{int a 10;int ra a;couta aendl;coutra raendl;return 0;
}在底层实现上实际是有空间的因为引用是按照指针方式来实现的。 int main()
{int a 0;int* p1 a;int ref a;return 0;
}转到反汇编代码引用和指针都有lea指令操作表示取地址的意思。 引用和指针的不同点:
引用概念上定义一个变量的别名指针存储一个变量地址。引用在定义时必须初始化指针没有要求。引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体。没有NULL引用但有NULL指针。在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)。引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小。有多级指针但是没有多级引用。访问实体方式不同指针需要显式解引用引用编译器自己处理。引用比指针使用起来相对更安全。
内联函数
下面是C语言中的一个Add宏函数
#define Add(x, y) ((x) (y))我们在C语言时期学习的宏函数没有类型的严格限制以及针对频繁调用较小的函数不需要再建立栈帧提升了效率。但是同样具有很多缺点 比如代码可读性差、可维护性差、语法坑很多不能对类型进行检查、不能调试。 C有哪些技术替代宏
常量定义 换用const enum短小函数定义 换用inline内联函数
inline概念
在C中以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销内联函数提升程序运行的效率。
在add函数前增加inline关键字将其改成内联函数在编译期间编译器会用函数体替换函数的调用。
inline int add(int x, int y)
{return x y;
}
int main()
{int ret add(1, 2);cout ret endl;return 0;
}需要对一些参数做修改后方便查看
在release模式下查看编译器生成的汇编代码中是否存在call Add在debug模式下需要对编译器进行设置否则不会展开(因为debug模式下编译器默认不会对代码进行优化以下给出vs2013的设置方式
特性
inline是一种以空间换时间的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率。inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议将函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下图为《Cprimer》第五版关于inline的建议: inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。解决方法我们直接定义就行。
// 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) (?fYAXHZ)该符号在函数 _main 中被引用auto关键字(C11)
类型别名思考
随着程序越来越复杂程序中用到的类型也越来越复杂经常体现在
类型难于拼写含义不明确导致容易出错
#include string
#include map
int main()
{std::mapstd::string, std::string m{ { apple, 苹果 }, { orange,
橙子 },{pear,梨} };std::mapstd::string, std::string::iterator it m.begin();while (it ! m.end()){//....}return 0;
}std::mapstd::string, std::string::iterator 是一个类型但是该类型太长了特别容易写错。聪明的同学可能已经想到可以通过typedef给类型取别名比如
typedef std::mapstd::string, std::string Map;使用typedef给类型取别名确实可以简化代码但是typedef有会遇到新的难题
typedef char* pstring;
int main()
{const pstring p1; // 编译成功还是失败const pstring* p2; // 编译成功还是失败、return 0;
}在编程时常常需要把表达式的值赋值给变量这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易因此C11给auto赋予了新的含义。
auto简介
在早期C/C中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量但遗憾的是一直没有人去使用它大家可思考下为什么
C11中标准委员会赋予了auto全新的含义即auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{return 10;
}
int main()
{int a 10;auto b a;auto c a;auto d TestAuto();cout typeid(b).name() endl;cout typeid(c).name() endl;cout typeid(d).name() endl;//auto e; 无法通过编译使用auto定义变量时必须对其进行初始化return 0;
}注意⚠️使用auto定义变量时必须对其进行初始化在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明而是一个类型声明时的“占位符”编译器在编译期会将auto替换为变量实际的类型。
auto的使用规则
auto与指针和引用结合起来使用 用auto声明指针类型时用auto和auto*没有任何区别但用auto声明引用类型时则必须加
int main()
{int x 10;auto a x;auto* b x;auto c x;cout typeid(a).name() endl;cout typeid(b).name() endl;cout typeid(c).name() endl;*a 20;*b 30;c 40;return 0;
}在同一行定义多个变量 当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错因为编译器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量。
void TestAuto()
{auto a 1, b 2;auto c 3, d 4.0; // 该行代码会编译失败因为c和d的初始化表达式类型不同
}auto不能推导的场景
auto不能作为函数的参数
// 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}auto不能直接用来声明数组
void TestAuto()
{int a[] {1,2,3};auto b[] {456};
}为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法。auto在实际中最常见的优势用法就是跟以后会讲到的C11提供的新式for循环还有lambda表达式等进行配合使用。
基于范围的for循环(C11)
范围for的语法
在C98中如果要遍历一个数组可以按照以下方式进行
void TestFor()
{int array[] { 1, 2, 3, 4, 5 };for (int i 0; i sizeof(array) / sizeof(array[0]); i)array[i] * 2;for (int* p array; p array sizeof(array)/ sizeof(array[0]); p)cout *p endl;
}对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因此C11中引入了基于范围的for循环。for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。
void TestFor()
{int array[] { 1, 2, 3, 4, 5 };//依次取数组中的数据进行赋值//auto 数组中数据的别名修改e等同于可以修改数组元素本身//自动判断结束自动迭代for(auto e : array)e * 2;for(auto e : array)cout e ;return 0;
}⚠️注意
与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。for循环迭代的范围必须是确定的对于数组而言就是数组中第一个元素和最后一个元素的范围。
空指针nullptr(C11)
C98 中的指针空值
在良好的C/C编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现不可预料的错误比如未初始化的指针。如果一个指针没有合法的指向我们基本都是按照如下方式对其进行初始化
void TestPtr()
{int* p1 NULL;int* p2 0;// ……
}NULL实际是一个宏在传统的C头文件(stddef.h)中可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif可以看到NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量。不论采取何种定义在使用空值的指针时都不可避免的会遇到一些麻烦比如
void f(int)
{coutf(int)endl;
}
void f(int*)
{coutf(int*)endl;
}
int main()
{f(0);f(NULL);f((int*)NULL);return 0;
}程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的初衷相悖。 在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void*)0。 ⚠️注意
在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入 的。在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr