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

🔥 脏脏a的技术站 🔥

「在代码的世界里,脏脏的技术探索从不设限~」

🚀 个人主页:脏脏a-CSDN博客

📌 技术聚焦:C++发展史、命名空间、输入输出、缺省参数、函数重载

📊 文章专栏:C++

🔗 上篇回顾:【上篇】C++入门

目录

一、引用

[1.1 引用概念](#1.1 引用概念)

[1.2 引用的特性](#1.2 引用的特性)

[1.3 常引用](#1.3 常引用)

[1.4 使用场景](#1.4 使用场景)

[1.4.1 引用做参数](#1.4.1 引用做参数)

[1.4.2 引用做返回值](#1.4.2 引用做返回值)

[1.5. 传引用、传值效率比较](#1.5. 传引用、传值效率比较)

[1.6 指针和引用的区别](#1.6 指针和引用的区别)

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

二、内联函数

[2.1 内联函数是啥?](#2.1 内联函数是啥?)

[2.2 如何判断是否为内联函数?](#2.2 如何判断是否为内联函数?)

[2.3 内联函数特性](#2.3 内联函数特性)

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

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

[3.1 auto 简介](#3.1 auto 简介)

[3.2 auto 的使用细则](#3.2 auto 的使用细则)

[3.2.1 auto 与指针和引用结合起来使用](#3.2.1 auto 与指针和引用结合起来使用)

[3.2.2 在同一行定义多个变量](#3.2.2 在同一行定义多个变量)

[3.3 auto 不能推导的场景](#3.3 auto 不能推导的场景)

[3.3.1 auto 不能作为函数的参数](#3.3.1 auto 不能作为函数的参数)

[3.3.2 auto 不能直接用来声明数组](#3.3.2 auto 不能直接用来声明数组)

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

[4.1 范围 for 的语法](#4.1 范围 for 的语法)

[4.2 范围 for 的使用条件](#4.2 范围 for 的使用条件)

[4.2.1 for 循环迭代的范围必须是确定的](#4.2.1 for 循环迭代的范围必须是确定的)

[4.2.2 迭代的对象要实现 ++ 和 == 的操作](#4.2.2 迭代的对象要实现 ++ 和 == 的操作)

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

问题1:NULL的问题?

问题2:为啥引入nullptr?

问题3:nullptr类型?


一、引用

1.1 引用概念

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

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

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

1.2 引用的特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
cpp 复制代码
#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 使用场景

1.4.1 引用做参数
cpp 复制代码
void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

【作用】:

  1. 输出型参数:传参时不用担心值拷贝问题,因为形参和实参都指向同一实体
  2. 提高效率:传递大对象时,只需传递对象的引用(而非拷贝整个对象),避免了大对象拷贝带来的性能开销,从而提高程序运行效率。
1.4.2 引用做返回值
cpp 复制代码
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指向的就是随机值了

【下面的也同理】:

【正确做法】:

cpp 复制代码
int& Count()
{
   static int n = 0;
   n++;
   // ...
   return n;
}

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

  1. 当用普通变量 int 接收函数返回的引用时,会拷贝 静态变量 n 的当前值

  2. 当用引用 int& 接收时,ret 会成为静态变量 n别名
    【作用】:

  3. 避免返回对象时的拷贝,提升性能(尤其对大对象);

  4. 允许直接修改被引用的原始对象(通过返回的引用可链式操作或修改源数据)。
    【注意】:

  • 若被引用对象销毁(如栈区局部变量),引用会变成 "悬空引用"(Dangling Reference),此时访问 / 修改会导致未定义行为(崩溃、数据错乱等)。

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

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

cpp 复制代码
#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;
}

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

【测试引用做返回值】:

cpp 复制代码
#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 指针和引用的区别

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

cpp 复制代码
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. 引用比指针使用起来相对更安全

sizeof 作用于引用时,会被编译器转换为对被引用对象的 sizeof 计算(即等价于 sizeof(int)

二、内联函数

2.1 内联函数是啥?

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

【内联函数调用汇编】:

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

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
  2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2022的设置方式)

1、右键项目,点击属性

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

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

2.3 内联函数特性

  1. inline 是一种 以空间换时间 的做法,如果编译器将函数当成内联函数处理,在 编译阶段,会
    用函数体替换函数调用 ,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
    行效率。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同 ,一般建
    议:将 函数规模较小 ( 即函数不是很长,具体没有准确的说法,取决于编译器内部实现 ) 、
    是递归、且频繁调用 的函数采用 inline 修饰,否则编译器会忽略 inline 特性。下图为
    《 C++prime 》第五版关于 inline 的建议:
  3. inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址
    了,链接就会找不到 (链接不上的核心场景就是"跨源文件调用且声明与定义分离)
  4. 在 C++ 中,由于内联函数默认具有内部链接属性且编译器在各编译单元内独立完成内联展开,因此不同源文件可定义同名但实现不同的内联函数且不会引发链接冲突。
  5. 内联函数的替换 是在 编译阶段 完成的
    **【问题】:**为啥内联函数可能会导致目标文件变大

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

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

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

【优点】:

  1. 增强代码的复用性。
  2. 提高性能。

【缺点】:

  1. 不方便调试宏。(因为预编译阶段进行了替换)
  2. 导致代码可读性差,可维护性差,容易误用。
  3. 没有类型安全的检查 。

C++有哪些技术替代宏

  1. 常量定义 换用const enum
  2. 短小函数定义 换用内联函数

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

3.1 auto 简介

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

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

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

3.2 auto 的使用细则

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

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

cpp 复制代码
int x = 20;
auto* p1 = &x;  // p1推导为int*
auto p2 = &x;   // p2同样推导为int*

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

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

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

cpp 复制代码
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不能用于函数形参的声明:

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

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

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

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

4.1 范围 for 的语法

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

cpp 复制代码
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 循环语法简洁,由冒号 ":" 分为迭代变量和被迭代范围两部分,示例:

cpp 复制代码
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方法来界定迭代范围。如下代码因范围不确定会出问题:

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

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

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

问题1:NULL的问题?

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

cpp 复制代码
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

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

cpp 复制代码
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)。

相关推荐
毕设源码-林学长7 小时前
计算机毕业设计java和Vue的安全教育科普平台设计与实现 安全知识普及与教育平台 安全教育信息化管理平台
java·开发语言·课程设计
AA陈超7 小时前
虚幻引擎5 GAS开发俯视角RPG游戏 P06-28 构建属性菜单小部件控制器
c++·游戏·ue5·游戏引擎·虚幻
恒者走天下7 小时前
cpp / c++零基础就业学习一站式学习平台
开发语言·c++·学习
Python私教7 小时前
Rust 编程语言基础知识全面介绍
开发语言·后端·rust
qq_433554548 小时前
C++ 单调栈
数据结构·c++·算法
向前阿、8 小时前
数据结构从基础到实战——排序
c语言·开发语言·数据结构·程序人生·算法
fpcc8 小时前
计算机原理—缓存
c++·缓存
lsx2024068 小时前
Ruby CGI Cookie 使用指南
开发语言
musenh8 小时前
javascript学习
开发语言·javascript·学习