开启C++之旅(下):引用、内联函数及现代特性(auto和范围for循环)

简介: 开启C++之旅(下):引用、内联函数及现代特性(auto和范围for循环)

1.引用


1.1引用概念


引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。通过引用,你可以使用一个变量的多个名称来访问和修改它的值


定义形式:


类型& 引用变量名 = 引用实体(这里&就不是c中大家熟知的取地址了)


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

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

二者也是共用一块内存空间


1.2引用特性


  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用(可以起多个别名)
  3. 引用一旦引用一个实体,再不能引用其他实体(不能改变指向)
int main()
{
  int a = 0;
  int& b = a;
  int c = 10;
  b = c;//那这个到底是   c变成d的别名?还是d赋值给c?
  return 0;
}

因为引用不能改变指向,这个是赋值


1.3常引用


int main()
{
  const int a = 10;
  int& ra = a;//这样编译器会报错
  return 0;
}

实际上这样会扩大权限:本身用const修饰后,不能改变a的值了,但是如果引用后就能利用引用改变。这样扩大了权限

这时使用常引用

int main()
{
  const int a = 10;
  const int& ra = a;//这样才对,没有扩大权限
  return 0;
}


其他情况


  1. 对非常量定义常引用
int main()
{
  int a = 10;
  const int& ra = a;//这样也可以,不会报错
  return 0;
}

权限缩小没问题

  1. 隐式类型转换,截断,强制类型转换

在 C++ 中,隐式类型转换、截断和强制类型转换都可能导致临时变量的创建,并且这些临时变量通常具有常量性质,那就需要用常引用

加了const就好了

在 C++ 中进行类型转换时,通常会创建一个临时变量来存储转换后的结果。这个临时变量是一个匿名对象,它存储了转换后的值,但并不会影响原始变量的值。这也是为什么对a进行类型转换后赋值,但是a不发生变化


1.4引用使用场景


1.4.1做参数


在函数中使用引用作为参数,可以让你直接操作传递给函数的变量,而不是对其进行复制。这样可以避免复制大型对象,提高效率,同时允许函数修改传递的变量值(可以简单理解为:我们把别名传了过来,当然能通过别名来改变本身)

void Swap(int& a, int& b)//交换两个整形
{
  int temp = b;
  b = a;
  a = temp;
}


1.4.2做返回值


在 C++ 中,函数可以返回引用,以避免在返回函数结果时产生拷贝。直接返回传递的变量的引用,允许你对该变量进行操作。然而,使用引用作为返回值需要小心,确保引用所指向的变量在函数返回后仍然有效

使用引用作为返回值的语法是在函数声明或定义中将函数返回类型声明为引用类型。

但是注意

  1. 不能返回局部变量的引用
int& Add(int a, int b)
{
  int c = a + b;
  return c;//返回了局部变量的引用
}
int main()
{
  //有问题的代码,这里不能用引用返回,否则为一个不确定的值
  int& ret = Add(1, 2);
  cout << "Add(1, 2) is :" << ret << endl;
  Add(3, 4);
  cout << "Add(1, 2) is :" << ret << endl;
  return 0;
}

在这里返回值是不是随机值,取决于是否清理栈帧!!!(没有清理就是原值,清理了就是随机的了)由于我是用VS编译器,VS出栈没有清理栈帧,所以导致这里打印出的是需要的到的值

ret始终是函数调用时使用的空间里变量c的别名

出了函数作用域,返回对象(局部变量)就销毁了,不能用引用返回,否则结果是不确定


最好返回指向全局变量、静态变量、或动态分配内存的引用,确保引用在函数返回后仍然有效


如果使用static来解决上述问题,一定把静态变量初始化和赋值分开

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

仍然有问题,因为

static 关键字用在局部变量上表示该变量在程序运行期间只初始化一次,后续再次调用时,不走那条语句了,直接return c

int& Add(int a, int b)
{
  static int c ;
  c = a + b;//分开就行了
  return c;
}


1.5引用与指针的区别


相信大家一开始都会抱有疑问,引用现在能做的指针不也都可以吗? 还有那个别名的底层是什么,怎么理解?

虽然在底层里,创建的引用变量实际是有空间的,可以通过汇编来观察引用是按照指针方式来实现

但是

在语法上:我们只是给那个空间取了一个别名没有开辟空间

int main()
{
  char a = '1';
  char& b = a;//如果开了空间,大小是4,没有就是1
  cout << sizeof(b);
  return 0;
}

其他区别:


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

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

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

没有NULL引用,但有NULL指针

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

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

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

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

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


2.内联函数


在学习c时,我们认识了宏


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


缺点: 1.不方便调试宏(因为预编译阶段进行了替换) 2.导致代码可读性差,可维护性差,容易误用。


3.没有类型安全的检查


为了解决缺点,c++中采用:


常量定义 换用const enum


短小函数定义 换用内联函数


2.1内联函数概念


inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销,内联函数提升程序运行的效率(用展开函数体来替代函数调用)

我们使用内联函数:


2.2内联函数特性


inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。 缺陷:可能会使目标文件变大。 优势:少了调用开销,提高程序运行效率。

inline对于编译器而言只是一个建议(会不会真的使用,看编译器自己决定)不同编译器关于inline实现机制可能不同,一般情况:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性

inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到


关于第三点:内联函数因为直接展开,也就不要地址查询(内联函数名不会进入符号表),我们之前经常在头文件里进行声明,一个源文件里面进行实现。现在在其他源文件里使用内联函数时不行的


3. auto关键字


随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型过长难于拼写
  2. 含义不明确导致容易出错

auto就是来解决这个问题


3.1概念


C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得


int main()
{
  auto a = 1;
  auto b = 1.1;
  auto c = 'c';//但这些都不是主要的使用场景
  cout << typeid(a).name() << endl;
  cout << typeid(b).name() << endl;
  cout << typeid(c).name() << endl;
  return 0;


3.2auto的使用细则


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

auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须&

在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量


3.3auto不能使用的场景


auto不能作为函数的参数

auto不能直接用来声明数组

为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用


4.基于范围的for循环(C++11)


4.1范围for的语法


之前我们写c的时候,在C++98中如果要遍历一个数组,可以按照以下方式进行:

void Test1()
{
  int arr[] = { 1, 2, 3, 4, 5 };
  for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
  {
    arr[i] *= 2;
  }
  for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); p++)
  {
    cout << *p << endl;
  }
}

现在我们可以这样:

void Test2()
{
  int array[] = { 1, 2, 3, 4, 5 };
  for (auto& e : array)//用for(int& e:array)也可以
  {
    e *= 2;
  }
  for (auto e : array)
  {
    cout << e << " ";
  }
}

C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围


基于范围的for循环会依次将容器中的元素赋值给迭代变量(通常命名为element)。在每次循环迭代中,迭代变量将会被赋值为容器中的下一个元素,直到遍历完整个容器


如果想要改变数组里,就使用引用

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


4.2范围for的使用条件


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

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围

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


5.指针空值nullptr(C++11)


我们经常使用的NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

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

可以看到,==NULL可能被定义为字面常量0,或者被定义为无类型指针(void)的常量==。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦*


所以我们使用nullptr来对指针进行初始化,来替代NULL,以免NULL定义为0时出现错误


注意:


在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr


好啦,c++入门的知识先到这里啦,下面就要开启面向对象的篇章了。感谢大家支持!!!


目录
相关文章
|
4月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
163 59
|
3月前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
32 2
|
4月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
4月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
4月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
63 0
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
47 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
48 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
38 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
47 4