拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C++ 从 C 到进阶 ②:引用、inline 、nullptr

目录1. 引言2. 引用Reference2.1引⽤的概念和定义2.2 引用的特性2.3 引用的使用2.4const引用2.5指针和引用的关系3. 内联函数inline4. 空指针常量 nullptr5. 总结1. 引言在上一篇中我们学习了命名空间、输入输出流、缺省参数和函数重载这四个 C 从 C 升级而来的核心特性。本文将延续这个话题继续讲解三个同样重要且实用的特性引用reference、inline 函数和nullptr。引用是 C 中一种全新的变量类型它比指针更安全、使用更便捷在某些场景下可以完美替代指针inline 函数用于优化程序性能是 C 语言中宏定义的更优替代方案nullptr则用于解决 C 语言中 NULL 存在的二义性问题掌握这三个特性你的 C 代码将更加安全、高效和规范。2. 引用Reference2.1引⽤的概念和定义引⽤不是新定义⼀个变量⽽是给已存在变量取了⼀个别名编译器不会为引⽤变量开辟内存空间 它和它引⽤的变量共⽤同⼀块内存空间。语法格式类型 引用别名 引用对象示例int ra a;//ra是a的引用别名引用和指针的形式区别int ra a; //引用别名 int *ra a; //指针存储地址实际应用示例#includeiostream using namespace std; int main() { int a 0; int ra a; //这里ra和rb是a的别名 int rb a; //这里ra和rb是a的别名一个变量可以用有多个引用 cout a endl; cout ra endl; cout rb endl; int rc rb; //也可以给别名起别名 cout rc endl; ra; //修改ra相当于修改a cout a endl; cout ra endl; cout rb endl; cout rc endl; return 0; }画图可以这么理解2.2 引用的特性1.引用在定义时必须初始化#includeiostream using namespace std; int main() { int a 10; int ra a; int b; int rb b; //int rc;//这里未进行初始化若未注释会报错rc必须初始化引用 return 0; }2.一个变量可以有多个引用#includeiostream using namespace std; int main() { int a 0; int ra a; int rb a;//这里ra和rb是a的别名 cout a endl; //0 cout ra endl; //0 cout rb endl; //0 int rc rb;//也可以给别名起别名 cout rc endl; //0 return 0; }3.引用一旦引用一个实体就不能再引用其他实体#includeiostream using namespace std; int main() { int a 0; int b 1; int ra a; // ra 是 a 的别名 cout a a endl; // a 0 cout b b endl; // b 1 cout 修改前 ra ra endl; // ra 0 // int ra b; // 错误ra 已经声明过了不能再次声明 // 这行如果取消注释编译器报错ra重定义多次初始化 ra b; // 注意这不是让 ra 指向 b // 而是把 b 的值1赋值给 ra 所引用的变量 a // 所以此时 a 变成了 1 cout 修改后 ra ra endl; // ra 1 cout a a endl; // a 1验证 a 确实被改了 return 0; }重要结论ra b;是赋值操作把b的值赋给ra所引用的变量a。引用不能像指针那样通过pa b;来改变指向的对象。引用从初始化那一刻起就终身绑定到那个对象无法更改。2.3 引用的使用1.引用在实践中主要是于引用传参和引用做返回值在类和对象中会了解中减少拷贝提高效率和改变引用对象时同时改变被引用对象。#includeiostream using namespace std; int main() { int a 0; int ra a; cout 修改前a a endl; //a 0 cout 修改前ra ra endl; //ra 0 ra; cout 修改后a a endl; //a 1 cout 修改后ra ra endl; //ra 1 //改变引用对象(ra)的同时被引用对象(a)也改变了 return 0; }2.引用传参跟指针传参功能是类似的引用传参相对更方便一些。引用传参非常典型的例子就是用一个函数交换两个变量的值之前我们都是采用的是指针的方式下面我们来对比一下指针和引用这两种方法1.指针的方法#includeiostream using namespace std; void Swap(int* a, int* b) { int tmp *a;//将a所指向位置的值(0)储存在tmp中 *a *b;//将b的指向位置的值1给a指向的空间中的值使得a存储的值变为了1 *b tmp;//将tmp所存储的值给b所指向位置的值使得b存储的值变为0 } int main() { int a 0; int b 1; cout 修改前a a endl; cout 修改前b b endl; Swap(a, b); cout 修改后a a endl; cout 修改后b b endl; return 0; }2.引用的方法#includeiostream using namespace std; void Swap(int ra, int rb) { int tmp ra; ra rb; //此处是将rb的值拷贝到了ra他们的指向没有发生变化 rb tmp; } int main() { int a 0; int b 1; cout 修改前a a endl; cout 修改前b b endl; Swap( a, b); cout 修改后a a endl; cout 修改后b b endl; return 0; }引用的方法是不是更简便些了呢小编在这里建议写引用的方法但是指针交换数值也要理解特定场景下可能也需要使用哦~~3.引用和指针在实践中相辅相成功能有重叠性但是各有特点互相不可替代。C的引用跟其他语言的引用(如Java)是有很大的区别的除了用法最大的点C引用定义后不能改变指向 Java的引用可以改变指向。注意对于双指针问题不能使用这种引用方法替代指针这种写法是错误的。// ❌ 注意 在 C 中是右值引用不能用来替代二级指针 void Func(int** pp) { } // 二级指针 // void Func(int rr) { } // 这是右值引用与二级指针无关 // ✅ 如果要替代部分二级指针场景应该使用指针的引用 void Func(int* rp) { } // 指针的引用可以替代部分二级指针场景2.4const引用1.可以引用⼀个const对象但是必须用const引用。const引用也可以引用普通对象因为对象的访问权限在引用过程中可以缩小但是不能放大。在c语言中const是一个关键字用于定义常量那么const引用本身只能读取数据不能修改数据。#includeiostream using namespace std; int main() { const int b 10; //这里的b只能读不能修改b是常量常量不能修改 const int rb b; //const可以引用一个const对象这里必须用const引用 //int rc b; //rc在这里可以读可以修改但b本身不能修改这就是权限放大了是错误的 const int c 10; //10是常量字面量const引用可以绑定到常量 int a 0; int ra a; //这里ra可以读可以修改 const int rd a; //这里的rb只可读不可以修改那么在这里就是权限缩小是正确的 return 0; }要充分运用const引用我们要先了解一下临时对象2.所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象 C中把这个未命名对象叫做临时对象。下面五种情况下会产生临时对象2和5是在类与对象中会了解到表达式运算调用一个函数函数传值返回类型转换常量返回临时对象的函数#includeiostream using namespace std; int main() { int a 0; //0表示的是一个常量 int b 1; int rb (a b); //ab这种就是表达式运算 double d 12.34; int c d; //double类型转换为int类型是类型转换 return 0; }这里我没有使用引用来表示这些量普通引用直接运用是错误的就需要使用const引用。3.C规定临时对象具有常性所以这里就触发了权限放大·必须要用常引用const引用才可以。#includeiostream using namespace std; int main() { int a 1; int b 2; //int rb (a b);//ab是表达式运算需要存储在临时对象中 //临时对象具有常性只能读不能修改 //但是rb可以读也可以修改就是权限放大 //在此我们就需要运用const来限制只能读不能修改 const int rb (a b);//这种是有出现临时对象时正确形式的书写形式 cout a endl; cout b endl; cout rb endl; return 0; }图像我们可以这么理解ra销毁后临时对象才会销毁所以不用担心指向的是空指针的问题。2.5指针和引用的关系语法概念上引用是⼀个变量的取别名不开空间指针是存储⼀个变量地址要开空间。引用在定义时必须初始化指针建议初始化但是语法上不是必须的。引用在初始化时引⽤⼀个对象后就不能再引⽤其他对象⽽指针可以在不断地改变指向对象。引⽤可以直接访问指向对象指针需要解引⽤才是访问指向对象。sizeof中含义不同引⽤结果为引用类型的⼤⼩即所引用对象的大小但指针始终是地址空间所占字节个数(32位平台下 占4个字节64位下是8byte)指针很容易出现空指针和野指针的问题引⽤很少出现引⽤使⽤起来相对更安全⼀些。#includeiostream using namespace std; int main() { int a 0; int* p a; *p 1;//指针需要解引用才能访问对象 int b 2; int rb b; rb 3;//引用可以直接访问对象 return 0; }3. 内联函数inline1.⽤inline修饰的函数叫做内联函数编译时C编译器会在调用的地方展开内联函数这样调用内联函数就需要建立栈帧了就可以提高效率。2.inline对于编译器而言只是⼀个建议也就是说你加了inline编译器也可以选择在调用的地方不展开不同编译器关于inline什么情况展开各不相同因为C标准没有规定这个。inline适用于频繁调用的短小函数1-10行对于递归函数代码相对多⼀些的函数加上inline也会被编译器忽略。展开 vs 不展开的区别不展开函数体代码 100 条指令调用 1000 次总指令 ≈ 100 1000 次调用开销展开每次调用都要复制 100 条指令总指令 ≈ 100 × 1000 100000 条指令代码膨胀3.C语言实现宏函数也会在预处理时替换展开但是宏函数实现很复杂很容易出错的且不方便调 试C设计了inline目的就是替代C的宏函数。ADD函数用宏的写法#includeiostream using namespace std; //宏的写法 #define ADD(a,b) ((a) (b)) //对于宏的写法有三个问题 //1.为什么不能加分号 //2.为什么要加外部的括号 //3.为什么要加内部的括号 int main() { int ret ADD(1, 2); //这里相当于int ret ((1) (2)) 3; cout ADD(1, 2) endl; //3 //宏定义是文本替换加分号展开后变成 cout ((1) (2)); endl;出现语法错误。 cout ADD(1, 2) * 5 endl; //如果不加外部的括号就是(1)(2)*511 cout ADD(1 2, 1 | 2) endl; //如果不加内部的括号就是(121|2)3,与我们要计算的不一样 //不同形式下由于比较级不同会导致不同的结果 return 0; }用inline的写法#includeiostream using namespace std; //inline的写法 inline int ADD(int a, int b) { int ret a b; return ret; } int main() { int ret ADD(1, 2); cout ADD(1, 2) endl; //3 cout ADD(1, 2) * 5 endl; //15 cout ADD(1 2, 1|2) endl; //3 return 0; }4.vs编译器debug版本下面默认是不展开inline的这样方便调试debug版本想展开需要设置⼀下以下两个地方。在项目属性最后一行中5.inline不建议声明和定义分离到两个文件分离会导致链接错误。因为inline被展开就没有函数地址链接时会出现报错。如果把inline函数的声明放在.h文件定义放在.cpp文件其他文件在调用时只能看到声明找不到函数地址因为展开后没有生成符号链接时就会报无法解析的外部符号错误。✅正确做法将inline函数的声明和定义都放在同一个.h头文件中。4. 空指针常量 nullptrNULL实际是⼀个宏在传统的C头⽂件(stddef.h)中可以看到如下代码#ifndef NULL #ifdef __cplusplus #define NULL 0 //c中NULL被定义为0 #else #define NULL ((void*) 0) //c语言中被定义为void*类型的0, //在 C 语言中NULL 是 (void*)0可以隐式转换为其他指针类型比如 int* p1 NULL; 是合法的。 #endif #endif1.C中NULL可能被定义为字⾯常量0或者C中被定义为⽆类型指针(void*)的常量。不论采取何种 定义在使⽤空值的指针时都不可避免的会遇到⼀些麻烦本想通过f(NULL)调⽤指针版本的 f(int*)函数但是由于NULL被定义成0调⽤了f(intx)因此与程序的初衷相悖。f((void*)NULL); 调⽤会报错。在下面这种形式中我们感觉的和实际上是不一样的#includeiostream using namespace std; void func(int x) { cout func(int x) endl; } void func(int* x) { cout func(int* x) endl; } //这两个函数参数的类型不同为函数重载 int main() { func(0);//这里是第一个函数 func(NULL);//这里也是第一个函数 return 0; }我们之前学习 C 语言时认为NULL是指针类型但在 C 中NULL被宏定义为整型0。而且 C 中void*不能隐式转换为int*所以即使想通过强制类型转换来解决也不行这样会导致类型不匹配。2.C11中引⼊nullptrnullptr是⼀个特殊的关键字nullptr是⼀种特殊类型的字面量它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题因为nullptr只能被隐式地转换为指针类型而不能被转换为整数类型。int main() { func(0); // 调用 void func(int x) func(NULL); // 调用 void func(int x)NULL 是 0 func(nullptr); // ✅ 调用 void func(int* x)nullptr 匹配指针版本 return 0; }在main函数中添加func(nullptr);就会调用指针版本的void func(int* x)函数因为nullptr只能匹配指针类型。5. 总结本篇我们继续学习了 C 从 C 升级而来的三个核心特性下面是知识点的快速回顾特性核心作用要点记忆引用给已存在变量取别名不开辟新内存必须初始化、一个变量可有多个引用、一旦引用不能更改绑定const引用可绑定常量和临时对象传参更简洁推荐替代指针传参inline函数在调用处展开减少函数调用开销适用于频繁调用的短小函数对编译器仅为建议声明和定义需放在同一头文件替代C语言宏函数nullptr表示空指针C11引入的关键字解决NULL在重载中的二义性问题只能隐式转换为指针类型不能转为整数类型关键理解引用通过别名机制让代码更简洁安全传参时无需取地址和解引用是C中强烈推荐的特性inline是宏定义的现代化替代方案保留了宏展开的优势同时弥补了类型检查、可调试等不足nullptr解决了NULL历史遗留的二义性问题让空指针的表达更明确、更类型安全 下篇预告《C 从 C 到进阶 ③类和对象上》将正式进入C面向对象编程的大门深入讲解类的定义、访问限定符、实例化、对象大小以及this指针最后对比C语言和C实现Stack的区别。如果你觉得这篇对你有帮助欢迎点赞 收藏 ⭐我们下篇见
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门