C++语法基础(下) 目录一. 缺省参数1. 全缺省参数2. 半缺省参数二. 引用1. 引用的概念与定义2. 引用的特性3. 引用的使用3.1 引用传参3.2 引用返回值4. const 引用4.1 避免数据拷贝4.2 保证数据不可变性5. 引用与指针对比三. nullptr1. NULL 与 nullptr 的区别四. inline1. 内联函数的概念与作用2. inline 是建议而非强制3. inline 对宏函数的替代4. vs2022 debug 版本默认不展开5. inline 不建议声明与定义分离6. 内联函数与宏函数对比注在看本文是如果感觉内容有点突兀请先浏览《C语法基础上》这篇文章帮助更好理解一.缺省参数缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参缺省参数分为全缺省和半缺省参数。有些地方把缺省参数也叫默认参数•全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值。C 规定半缺省参数必须从右往左依次连续缺省不能间隔跳跃给缺省值。示例代码#includeiostream using namespace std; //全缺省参数 int add(int x 4, int y 3) { cout x y ; return x y; } //半缺省参数 int sub(int x, int y2) { cout x - y ; return x - y; } int main() { int a 1, b 2; //全缺省时不传参vs传参 cout add() endl; cout add(a, b) endl; //半缺省是时,不传参vs传参 cout sub(5) endl; cout sub(a, b) endl; return 0; }以上代码展示了全缺省参数和半缺省参数的函数定义与使用。 add 函数是全缺省参数示例 sub 函数为半缺省参数示例。通过在 main 函数中调用呈现不传参时使用缺省值传参时使用指定实参的效果。• 带缺省参数的函数调用C 规定必须从左到右依次给实参不能跳跃给实参。• 函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现规定必须函数声明给缺省值。二.引用1.引用的概念与定义引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。比如《水浒传》中李逵宋江叫“铁牛”江湖上人称“黑旋风”林冲外号豹子头。所以一个引用也可以有多个别名。类型 引用别名 引用对象;#includeiostream using namespace std; int main() { int a 2; // b、c 都是 a 的引用别名 int b a; int c a; // 通过 c 自增a/b/c 同步变化 c; cout a b c endl; return 0; }以上C 代码在 main 函数中先定义了变量 a 并赋值为2 。接着定义了 b 和 c 作为 a 的引用由于引用与被引用变量共用内存空间对 c 执行自增操作后 a 、 b 、 c 的值都变为3 运行结果验证C 中引用的这一特性。2.引用的特性①引用在定义时必须初始化②一个变量可以有多个引用③引用一旦引用一个实体再不能引用其他实体3.引用的使用在C 实践中引用主要发挥于引用传参与引用返回值两大场景-引用传参与指针传参功能近似却更具便捷性。通过引用传递参数可规避对象拷贝提升程序运行效率且能直接操作实参改变其值。-引用返回值应用场景较为复杂这里先作简要介绍后续在类和对象章节将深入探讨。它同样能减少拷贝开销有时还可用于实现对特定对象的持续引用操作 。引用与指针在实际运用中相互配合虽功能有所重叠但各自特性鲜明无法相互取代。值得注意的是C 的引用与Java 等语言的引用差异显著。C 中引用一经定义便无法更改指向而Java 里的引用则无此限制可灵活变更指向。部分以C 代码为主的《数据结构》教材会采用C 引用替换指针传参旨在简化程序逻辑规避指针操作的复杂性。引用传参和指针传参#includeiostream using namespace std; // 引用传参 void Swap(int x, int y) { int tmp x; x y; y tmp; } // 指针传参 void Swap(int* x, int* y) { int tmp *x; *x *y; *y tmp; } void test() { int a13,a2 3, b14,b2 4; cout a1 b1 endl; // 引用方式交换 Swap(a1, b1); cout a1 b1 endl; cout a2 b2 endl; // 指针方式交换 Swap(a2, b2); cout a2 b2 endl; } int main() { test(); return 0; }以上C 代码展示了两种实现交换功能的函数一个通过引用传参 Swap(int x, int y) 另一个通过指针传参 Swap(int* x, int* y) 。在 test 函数中对这两个函数进行调用测试分别传入变量和变量地址。运行结果表明二者都能成功实现变量值的交换直观体现引用和指针在参数传递用于修改实参值时的作用。4.const引用在C 里 const 引用即常量引用它是对变量的只读引用但不能赋值。从定义来讲当用 const 修饰引用时就创建了这种只读关系一旦绑定到某个对象通过该引用不能修改所绑定对象的值 。比如 const int ret a; 此后不能执行 ret 新值; 这样的操作 。其主要作用体现在两方面①避免数据拷贝在函数参数传递中若传递大对象按值传递会产生拷贝开销使用 const 引用传递能避免拷贝提升程序性能 。例如函数 void func(const 大对象类型 obj) 调用时不会对实参进行拷贝 。②保证数据不可变性能确保在函数内部不会意外修改引用的数据为数据安全性提供保障 。在多函数调用、多人协作开发等场景下可防止数据被误改让代码逻辑更清晰、可靠 。此外 const 引用还有个特殊之处它可以和声明类型不匹配 比如 const int r 3.14; 编译器会创建临时量来实现转换和绑定 但普通引用不允许这样做 。以下示例#includeiostream using namespace std; int main() { int a 2; const int ra a; //ra;ra不能自加 a; cout a endl; cout ra endl; return 0; }代码展示了 const 引用的特性。在 main 函数中定义变量 a 并赋值为2 接着创建 const int ra 作为 a 的常量引用。由于 ra 是 const 引用不能对其执行自增操作如 ra 被注释掉 但原变量 a 可以自增。运行结果显示 a 自增后的值为3 ra 的值也同步变为3 体现 const 引用只读且与原变量关联的特性。#includeiostream using namespace std; int main() { // const 引用可直接绑定字面量 const int a 2; int b 3; // const 引用只读不能通过 ra 修改 a const int ra a; const int rb b; //但是int ra a;这种写法是错误的 }这段 代码在 main 函数中先定义了常量 a 和变量 b 然后分别创建了它们的 const 引用 ra 和 rb 。代码注释指出 int ra a; 这种写法是错误的 。因为 a 是 const 类型普通引用不能绑定到 const 对象上而 const 引用能确保不会通过引用意外修改对象的值此例直观体现const 引用的使用规则和对象绑定限制。#includeiostream using namespace std; int main() { int a 10; const int ra 30; // 编译报错初始化无法从int转换为int // int rb a * 3; // const 引用可绑定表达式结果 const int rb a * 3; double d 12.34; // 编译报错初始化无法从double转换为int // int rd d; const int rd d; return 0; }这段 代码在 main 函数中展示了 const 引用的特殊用法及普通引用的限制。定义变量 a 后 const int ra 30; 直接绑定字面量体现 const 引用可绑定临时值。而 int rb a * 3; 等普通引用绑定临时值或类型不匹配值会编译报错。 const int rb a * 3; 、 const int rd d; 能成功表明 const 引用可在类型转换等情况下通过创建临时变量实现绑定凸显 const 引用与普通引用在对象绑定上的差异。5.引用与指针对比引用使用建议①函数参数传递当函数需要修改外部变量或希望避免值传递带来的对象拷贝开销时使用引用作为函数参数。比如在自定义类的赋值运算符重载函数ClassName operator(const ClassName other) 中用引用传递参数既避免拷贝又能实现对象间赋值。②简单数据别名需求若只是想给已存在变量起别名方便代码可读性和语义表达可使用引用 。如int num 10; int numRef num; 后续操作numRef就等同于操作num 。③范围for循环在C范围for循环中使用引用可避免遍历容器元素时的拷贝开销。如std::vector vec {1, 2, 3}; for(int element : vec) 直接操作容器内元素本身提高效率。④避免空值风险场景因引用不能为NULL在不希望出现空指针相关问题要求对象必须有效的场景优先考虑引用。三.nullptr在C 中 引入nullptr是为了替代NULL虽然两者都可以 用于表示指针为空的状态但两者存在差异。从类型角度 NULL 本质是整数 0 或预处理器宏定义为 0 常被视为整型虽可赋值给指针但会造成类型不匹配隐患。而 nullptr 是C11引入的关键字类型为 std::nullptr_t 专门用于表示空指针与任何指针类型都能隐式转换类型安全性更好。使用场景上 NULL 在C和早期C代码中广泛使用不过在C 中易导致函数重载歧义因它可被当成整数参与重载决议。 nullptr 则消除了这种歧义在C11及以后代码中推荐使用 nullptr 表示空指针使代码更清晰、安全符合现代C 类型系统规范。#includeiostream using namespace std; void f(int x) { cout f(int x) endl; } void f(int* ptr) { cout f(int* ptr) endl; } int main() { // 0 匹配 int 版本 f(0); // 本想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0调用了f(intx)因此与程序的初衷相悖。 f(NULL); f((int*)NULL); // 编译报错error C2665: f2 个重载中没有一个可以转换所有参数类型 // f((void*)NULL); // nullptr 匹配指针版本 f(nullptr); return 0; }这段C 代码定义了两个重载函数 f(int x) 和 f(int* ptr) 。在 main 函数中进行调用测试 f(0) 调用了 f(int x) 。原本想通过 f(NULL) 调用指针版本的函数却因 NULL 被定义为0 实际调用了 f(int x) 。 f((int*)NULL) 能正确调用指针版本而 f((void*)NULL) 编译报错。使用 nullptr 则可准确调用 f(int* ptr) 清晰展现函数重载下指针类型调用及 NULL 与 nullptr 的区别。四.inline1.用inline修饰的函数叫做内联函数编译时C编译器会在调用的地方展开内联函数这样调用内联函数就不需要建立栈帧了从而可以提高效率。2.inline 对于编译器而言只是一个建议也就是说即便加了 inline 编译器也可以选择在调用的地方不展开不同编译器关于 inline 在什么情况展开各不相同因为C标准没有规定这个。注inline适用于频繁调用的短小函数对于递归函数、代码相对多一些的函数加上 inline也会被编译器忽略。3. C语言实现宏函数也会在预处理时替换展开但是宏函数实现很复杂且容易出错并且不方便调试C设计了inline目的就是替代C的宏函数。4.vs2022编译器debug版本下默认是不展开 inline 的这样方便调试debug版本想展开需要设置一下以下两个地方。如下图所示5.inline不建议声明和定义分离到两个文件分离会导致链接错误。因为 inline 被展开就没有函数地址链接时会出现报错。inline在预处理作用// 内联展开无 call 指令直接计算 mov eax,dword ptr [a] mov dword ptr [rsp28h],eax mov eax,dword ptr [a] // a inc eax mov dword ptr [a],eax mov eax,dword ptr [rsp28h] mov ecx,dword ptr [a] // xy add ecx,eax mov eax,ecx mov dword ptr [rsp2Ch],eax mov eax,dword ptr [rsp2Ch] mov dword ptr [ret],eax// 非内联通过 call 调用 add 函数 mov eax,dword ptr [a] mov dword ptr [rsp28h],eax mov eax,dword ptr [a] inc eax mov dword ptr [a],eax mov edx,dword ptr [rsp28h] mov ecx,dword ptr [a] call add (07FF7A0991267h) mov dword ptr [ret],eax汇编指令共性两张图中的汇编指令有不少相同点像 mov 指令用于数据传送比如把内存中 a 地址处的数据传进寄存器或者把寄存器数据存到特定内存地址 inc 指令实现寄存器值自增 add 指令完成寄存器间加法运算。这些指令为理解代码底层操作奠定基础。两张图差异剖析①第一张图缺失 call 指令。这是 add 函数被内联展开所致。内联展开是编译器的一种优化手段它直接将 add 函数的代码体嵌入到调用 add 的地方。如此一来程序执行时就无需进行常规函数调用操作比如不用建立和销毁栈帧。这减少了函数调用的额外开销提高了代码执行效率直接在调用处就执行了原本 add 函数内的操作逻辑 。②第二张图有 call 指令。这表明是常规函数调用方式。当程序执行到 call add 时会暂停当前指令流跳转到 add 函数所在的内存地址这里是 07FF7A0991267h 去执行 add 函数的代码。在跳转前计算机会把返回地址等关键信息压入栈中等 add 函数执行完毕再依据栈中保存的返回地址回到调用 call add 指令的下一条指令处继续执行后续代码 。6.内联函数与宏函数对比