【C++ 入门】:引用、内联函数与 C++11 新特性(auto、范围 for、nullptr)全解析

【C++ 入门】:引用、内联函数与 C++11 新特性(auto、范围 for、nullptr)全解析

目录

一、引用

1.1 引用概念

1.2 引用的特性

1.3 常引用

1.4 使用场景

1.5. 传引用、传值效率比较

1.6  指针和引用的区别

【面试题】:引用和指针的对比

二、内联函数

2.1 内联函数是啥?

2.2 如何判断是否为内联函数?

2.3 内联函数特性

【问题】: 为啥内联函数可能会导致目标文件变大

【问题】:递归不能内联的核心原因

【面试题】:宏的优缺点?

【面试题】:内联函数的优缺点?

三、auto关键字(C++11)

3.1 auto 简介

3.2 auto 的使用细则

3.2.1 auto 与指针和引用结合起来使用

3.2.2 在同一行定义多个变量

3.3 auto 不能推导的场景

3.3.1 auto 不能作为函数的参数

3.3.2 auto 不能直接用来声明数组

四、基于范围的for循环(C++11)

4.1 范围 for 的语法

4.2 范围 for 的使用条件

4.2.1 for 循环迭代的范围必须是确定的

4.2.2 迭代的对象要实现 ++ 和 == 的操作

五、指针空值nullptr(C++11)

问题1:NULL的问题?

问题2:为啥引入nullptr?

问题3:nullptr类型?


一、引用

1.1 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"

void TestRef() {    int a = 10;    int& ra = a;//<====定义引用类型    printf("%p\n", &a);    printf("%p\n", &ra); }

注意:引用类型必须和引用实体是同种类型的


1.2 引用的特性

 1、引用在定义时必须初始化

 2、一个变量可以有多个引用

 3、引用一旦引用一个实体,再不能引用其他实体
#include<iostream> using namespace std; int main() { int a = 10; // 1、编译报错:“ra”: 必须初始化引⽤ //int& ra; //2、一个变量可以有多个引用 int& b = a; int& d = a; // 3、这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向, 这⾥是⼀个赋值 int c = 20; b = c; return 0; }

1.3 常引用

概念:const引用就是常引用 

常引用场景:

const 引用(常引用)绑定临时变量时,会延长临时变量的生命周期,使其与 const 引用的生命周期一致,直到该引用(如rii)的生命周期结束

 


1.4 使用场景

引用做参数:

void Swap(int& left, int& right) {   int temp = left;   left = right;   right = temp; }
输出型参数:传参时不用担心值拷贝问题,因为形参和实参都指向同一实体

提高效率:传递大对象时,只需传递对象的引用(而非拷贝整个对象),避免了大对象拷贝带来的性能开销,从而提高程序运行效率。

引用做返回值:

int& Add(int a, int b) {    int c = a + b;    return c; } int main() {    int& ret = Add(1, 2);    Add(3, 4);    cout << "Add(1, 2) is :"<< ret <<endl;    return 0; }

上面代码有个很明显的问题:返回了局部变量的引用,局部变量出了函数就销毁了,这里返回的ret的值有两种可能性,如果函数结束栈幁销毁,但是没有清理栈幁,那么ret引用可能指向被引用的局部变量,如果栈幁被清理,那么ret指向的就是随机值了

正确做法:

int& Count() {   static int n = 0;   n++;   // ...   return n; }

返回个静态区的局部变量,静态区局部变量的生命周期是整个程序,所以无论用int接收还是用int&接收都没有任何问题

1、接收返回引用的两种情况

用普通变量接收:会拷贝静态变量当前的值;

用引用变量接收:这个引用会成为静态变量的别名。

2、返回引用的优势

避免对象拷贝(尤其对大对象),提升性能;

可直接修改原始对象,支持链式操作。

3、返回引用的风险

若被引用对象(比如栈区局部变量)被销毁,会形成 “悬空引用”,此时访问或修改会引发崩溃、数据错乱等未定义行为。

1.5. 传引用、传值效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。下面我用代码测试一下传引用和传值,让大家更直观感受效率差别:测试引用做参数:

#include <time.h> struct A{ int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 1、以值作为函数参数 //记录当前时间 size_t begin1 = clock(); for (size_t i = 0; i < 1000000; ++i) TestFunc1(a); //记录传值调用的结束时间 size_t end1 = clock(); // 2、以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 1000000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } 

编译器默认单位是毫秒,从运行结果来看,效率差距是很大的

测试引用做返回值:

#include <time.h> struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { //1、 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); //2、 以引用作为函数的返回值类型 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; } int main() { TestReturnByRefOrValue(); return 0; }

1.6  指针和引用的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

int main() { int a = 10; int& ra = a; cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; return 0; }

底层实现上实际是有空间的,因为引用可能是按照指针方式来实现的(不同编译器的实现细节肯定不一样)vs2022下指针和引用的底层对比:

从汇编代码来看,底层是差不多的,但是也不能一概而论,毕竟每个编译器的实现不一样


【面试题】:引用和指针的不同点

1、引用概念上定义一个变量的别名,指针存储一个变量地址。

2、引用在定义时必须初始化,指针没有要求

3、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

4、没有NULL引用,但有NULL指针

5、在sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)

6、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

7、有多级指针,但是没有多级引用

8、访问实体方式不同,指针需要显式解引用,引用编译器自己处理

9、引用比指针使用起来相对更安全

二、内联函数

2.1 内联函数是啥?

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。普通函数调用汇编:

内联函数调用汇编:

2.2 如何判断是否为内联函数?

在release模式下,查看编译器生成的汇编代码中是否存在call Add

在debug模式下
,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2022的设置方式)

1、右键项目,点击属性

2、展开C/C++,把常规选项中的调试信息格式化改成程序数据库

3、优化中的内联函数拓展修改成只适用于_inline(/Ob1)


2.3 内联函数特性

核心:以空间换时间 —— 编译阶段用函数体替换调用,优势是减少调用开销、提升效率;缺陷是可能增大目标文件。

编译器的建议性inline是建议而非强制,编译器通常仅处理规模小、非递归、频繁调用的函数(如《C++prime》所述),大函数(如 75 行)或递归函数会被忽略。

声明定义不分离:分开写会导致链接错误(跨源文件调用时更明显),因为内联函数无独立地址。

内部链接属性:不同源文件可定义同名内联函数,不会引发链接冲突。

替换时机:编译阶段完成。


【问题】: 为啥内联函数可能会导致目标文件变大


【问题】:递归不能内联的核心原因

内联需编译时确定展开次数,而递归调用层次由运行时动态决定(依赖输入或状态),编译器无法预知,故无法安全展开。


【面试题】:宏的优缺点?

【面试题】:内联函数的优缺点?

三、auto关键字(C++11)

3.1 auto 简介

在 C++11 中,auto关键字被赋予了全新的含义 —— 作为编译期类型推导指示符。它不再表示 “自动存储期的变量”,而是让编译器根据变量的初始化表达式,自动推导出变量的实际类型。这一特性极大简化了复杂类型的声明,尤其在处理冗长的 STL 容器迭代器、模板类型等场景时,能显著提升代码的简洁性和可读性。

使用auto的核心要求是变量必须初始化,因为编译器需要通过初始化表达式才能完成类型推导。例如:

int a = 10; auto b = a; // 编译器推导出b的类型为int auto c = 'c';// 推导出c的类型为char 

3.2 auto 的使用细则

3.2.1 auto 与指针和引用结合起来使用

声明指针类型时,autoauto*的效果完全一致,均会推导出指针类型:

int x = 20; auto* p1 = &x; // p1推导为int* auto p2 = &x; // p2同样推导为int* 

声明引用类型时,必须显式添加&,否则auto会推导出被引用对象的类型而非引用:

int y = 30; auto& r = y; // r推导为int&(y的引用) auto r2 = y; // r2推导为int(y的值拷贝) 
3.2.2 在同一行定义多个变量

当在同一行使用auto声明多个变量时,所有变量必须能被推导为相同类型,否则会编译报错。这是因为auto仅能推导出一种类型,无法同时适配多种不同类型:

auto a = 10, b = 20; // 正确,a和b均推导为int // auto c = 10, d = 3.14; // 错误,c推导为int,d推导为double,类型不一致 

3.3 auto 不能推导的场景

3.3.1 auto 不能作为函数的参数

编译器无法在编译期根据函数调用情况推导出参数的实际类型,因此auto不能用于函数形参的声明:

// 编译失败:auto不能作为函数参数类型 void func(auto param) { // ... } 
3.3.2 auto 不能直接用来声明数组

auto无法推导出数组类型,因此不能直接用于数组的声明。若需简化数组相关的类型声明,可结合指针或引用间接实现:

int arr[] = {1, 2, 3}; // auto arr2[] = {4, 5, 6}; // 错误,auto不能直接声明数组 auto* p = arr; // 正确,p推导为int*(指向数组首元素) 

四、基于范围的for循环(C++11)

4.1 范围 for 的语法

在 C++98 中遍历数组需手动控制循环范围,如:

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

而 C++11 的范围 for 循环语法简洁,由冒号 “:” 分为迭代变量和被迭代范围两部分,示例:

void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for(auto& e : array) e *= 2; for(auto e : array) cout << e << " "; return 0; } 

它支持continue结束本次循环、break跳出整个循环,与普通循环逻辑一致。

4.2 范围 for 的使用条件

4.2.1 for 循环迭代的范围必须是确定的

对于数组,范围是数组第一个元素到最后一个元素;对于类,需提供beginend方法来界定迭代范围。如下代码因范围不确定会出问题:

void TestFor(int array[]) { for(auto& e : array) cout<< e <<endl; } 
4.2.2 迭代的对象要实现 ++ 和 == 的操作

迭代过程依赖这些操作来控制迭代逻辑(此部分涉及迭代器知识,后续会详细讲解,现阶段了解即可)。

五、指针空值nullptr(C++11)

问题1:NULL的问题?

NULL本质是宏,在传统 C 头文件 stddef.h 中定义如下:

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

即 NULL 可能被定义为字面常量 0 ,或无类型指针 (void*) 的常量 。

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(0) 调用 f(int) 没问题,但 f(NULL) 由于 NULL 定义的模糊性(既像 0 又像指针),可能导致编译器匹配混乱,而 f((int*)NULL) 虽然明确转化为指针类型调用 f(int*) ,但这种写法不够简洁直观 

问题2:为啥引入nullptr?

无需额外头文件:

nullptr 是 C++11 引入的新关键字,专门表示指针空值 。使用它时,无需包含额外头文件,代码简洁性提升。

字节数特性:

在 C++11 中,sizeof(nullptr) 与 sizeof((void*)0) 所占字节数相同 。这意味着 nullptr 在内存占用等底层特性上,和传统表示空指针的方式在字节层面有对应关系。

提升代码健壮性:

    相比 NULL 可能带来的歧义,nullptr 明确表示指针空值。在函数重载等场景下,能让编译器准确匹配函数,减少错误发生概率,使代码更健壮。例如之前的 f 函数调用,使用 nullptr 就很明确:​

总结来说,nullptr 作为 C++11 的新特性,解决了 C++98 中 NULL 表示指针空值的一些弊端,让指针空值的表达更清晰、准确,有助于写出更可靠的代码

问题3:nullptr类型?

nullptr 的类型是 std::nullptr_t,它可以隐式转换为任何指针类型(包括对象指针、函数指针等),但不能转换为整数类型(这一点和 NULL 不同,NULL 可能被解析为整数 0)。

Read more

Effective Modern C++ 条款40:深入理解 Atomic 与 Volatile 的多线程语义

Effective Modern C++ 条款40:深入理解 Atomic 与 Volatile 的多线程语义

Effective Modern C++ 条款40:深入理解 Atomic 与 Volatile 的多线程语义 * 1. Atomic 与 Volatile 的基本概念 * 1.1 Atomic 的原子性本质 * 1.2 Volatile 的特殊内存语义 * 2. 多线程环境下的表现对比 * 2.1 Atomic 的线程安全保障 * 2.2 Volatile 的线程不安全表现 * 2.3 任务通知场景对比 * 3. 内存模型与编译器优化 * 3.1 普通内存的编译器优化 * 3.2 特殊内存的处理 * 4. Atomic 的操作限制与解决方案 * 4.1 禁止的操作 * 4.

By Ne0inhk
C++ 游戏开发:从零到英雄的进阶之旅

C++ 游戏开发:从零到英雄的进阶之旅

在当今数字化时代,游戏开发已然成为极具吸引力与挑战性的领域。C++ 作为游戏开发中极为常用的语言之一,凭借其高性能和强大功能,长久以来都是游戏开发者的心头好。若你对游戏开发满怀热忱,却不知如何起步,这篇博客就将为你揭开 C++ 游戏开发的神秘面纱,引领你踏上从新手到高手的进阶之路。 一、为什么选择 C++ 进行游戏开发? 在游戏开发的广袤天地里,编程语言的抉择至关重要。C++ 以其独有的优势,成为众多开发者的不二之选: (一)高性能 游戏开发过程中需要处理海量的实时计算任务,涵盖图形渲染、物理模拟以及用户输入响应等关键环节。C++ 具备直接访问硬件的能力,能够极为高效地利用系统资源,切实保障游戏运行的流畅性。以处理复杂的 3D 场景渲染为例,C++ 能够快速对大量的顶点数据、纹理信息进行处理和计算,精准地将虚拟的 3D 世界呈现在玩家眼前,其性能优势在这种场景下展现得淋漓尽致。 (二)强大的功能 C++ 全力支持面向对象编程(OOP),这使得开发者能够通过类和对象来有条不紊地组织代码。比如在开发一款角色扮演游戏时,我们可以创建 “角色” 类,

By Ne0inhk

Visual C++运行库完整安装指南:解决“缺少DLL文件“问题

Visual C++运行库完整安装指南:解决"缺少DLL文件"问题 【免费下载链接】vcredistAIO Repack for latest Microsoft Visual C++ Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 当您打开游戏或专业软件时,是否遇到过"缺少MSVCP140.dll"、"VCRUNTIME140_1.dll丢失"等错误提示?这些问题通常是由于Visual C++ Redistributable运行库缺失或损坏导致的。本指南将为您提供从自动安装到手动修复的全套解决方案。 问题分析:为什么会出现DLL缺失错误 Visual C++运行库是Windows系统运行C++程序的基础组件,不同年份的软件需要对应版本的运行库支持。常见问题场景包括:

By Ne0inhk
【C++】————智能指针

【C++】————智能指针

作者主页:     作者主页                                                       本篇博客专栏:C++                                                       创作时间 :2024年8月20日 一,什么是智能指针 在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。 *  c++中用的最多的是下面三种智能指针 C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件<memory>std::s

By Ne0inhk