C++入门下(引用、内联函数、auto、范围for、nullptr)

简介: C++入门下(引用、内联函数、auto、范围for、nullptr)

😜前言😜

前面我们学习了C++的关键字、命名空间、输入输出、缺省参数、函数重载,今天我们再来学习C++中的引用、auto、nullptr、基于范围的for循环、内联函数,接下来我来给大家一一介绍以上内容。

😛引用😛

什么是引用?

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。编译器也不会为引用变量开辟内存空间,它和它引用的的变量共用哦个一块内存空间。


C++ 中创建引用

类型& 引用变量名(对象名) = 引用实体;

举个栗子>

int main()
{
  int a = 1;
  int& ra = a;//定义引用类型变量
  cout << a << endl;
  cout << ra << endl;
  cout << &a << endl;
  cout << &ra << endl;
  return 0;
}

运行结果> 


d5fdb738d8a14aee948e64d05f551bed.png


不难看出引用变量并没有申请内存空间,而是和引用对象共用同一块空间。

引用特性

  1. 引用在定义的时候必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,就不能引用其他实体。


int main()
{
  int a = 10;
  //int& ra;//编译器直接会报错
  int& ra = a;
  int& rra = ra;
  cout << &a << endl;
  cout << &ra << endl;
  cout << &rra << endl;
  return 0;
}


4d6fda21bb4045e9aa692bd337611827.png


常引用

int main()
{
  const int a = 10;
  //int& ra = a;//error:a为const修饰的常量
  const int& ra = a;
  //int& b = 10;//error:b为常量
  const int& b = 10;
  double dd = 1.2;
  //int& rd = d;//error:引用类型不同
  const int& rd = dd;
  return 0;
}


引用过程中权限可以平移或者减小,不能变大。


4738bb28af2440ddb0bcf0dd7f2ea4e5.png

引用的使用场景

1.做参数

我们以往的Swap交换函数要通过传地址,指针接受来实现>


void Swap(int* a, int* b)
{
  int tmp = *a;
  *a = *b;
  *b = tmp;
}
int main()
{
  int a = 1;
  int b = 2;
  cout << "修改前:";
  cout << " a = " << a ;
  cout << " b = " << b << endl;
  Swap(&a, &b);
  cout << "修改后:";
  cout << " a = " << a;
  cout << " b = " << b << endl;
  return 0;
}


cd31dcdffef14bf09619cea8f2a97a70.png


上面这种指针类型的每次都要传地址,会很麻烦,当我们学了引用我们可以这样做>


void Swap(int& a, int& b)
{
  int tmp = a;
  a = b; 
  b = tmp;
}
int main()
{
  int a = 1;
  int b = 2;
  cout << "修改前:";
  cout << " a = " << a ;
  cout << " b = " << b << endl;
  Swap(a, b);
  cout << "修改后:";
  cout << " a = " << a;
  cout << " b = " << b << endl;
  return 0;
}


8bd492eabb8241d7a3526bfd5dc3e362.png

使用引用同样也可以完成交换,而且使用引用的还不需要为形参开辟新的空间,形参和实参公用同一块空间。

2.做返回值


int& Count(int x)
{
  int n = x;
  n++;
  return n;
}
int main()
{
  int& ret = Count(10);
  cout << ret << endl;
  return 0;
}


上面这段函数按照逻辑来看,当Count函数调用结束函数栈帧会被销毁,如果传引用的话指向的还是被销毁的那块空间,Count函数栈帧销毁之后如果被清理了,那么最终ret将会是随机值,如果没有被清理,那么ret的值侥幸正确,输出11/随机值,我们来看看结果>

2f69529deef641f69c1e924db537c101.png

可以看到函数栈帧并没有被清理,我们来这样试试>

#include<stdlib.h>
int& Count(int x)
{
  int n = x;
  n++;
  return n;
}
int main()
{
  int& ret = Count(10);
  rand();
  cout << ret << endl;
  return 0;
}


在ret接收之后调用库函数rand();我们再来看看结果>


4693af8d05014514800a0c836849b642.png


这次ret的结果就不是11了,而是随机值,是因为Count函数的栈帧被销毁之后,又调用了rand()函数,操作系统再为rand函数分配栈帧,同时ret所指向的地址有可能就会被覆盖导致ret出现随机值。


fac1c6b134d3403f92e63cb88993d118.png


再看一个栗子>

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

3c7f3197660247df8f219ea34e72cc9b.png

画图分析


a28d054d849046da99484a7098858088.png


如果返回的对象还在(没有还给操作系统)那么就可以使用引用返回。

举个栗子>

#include<stdlib.h>
int& Count(int x)
{
  static int n = x;
  n++;
  return n;
}
int main()
{
  int& ret = Count(10);
  cout << ret << endl;
  rand();
  cout << ret << endl;
  return 0;
}

88a088e1ea4648c2830ee399ae8e6568.png

在Count函数中把n定义为static(静态变量)类型的静态变量,那么n就会在静态区中开辟空间,静态区直到程序最终结束才会销毁。

总结:

  1. 基本任何场景都可以用引用传参。
  2. 谨慎用引用作为返回值,出了作用域,对象不在了,就不能用引用返回,如果还在就可以用引用返回


53549f0494164c798a5c7848b7fe942f.png


传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者讲变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大的时候,效率就会更低。


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



9ad7be063b4b40da8acda6e31ba0da2e.png

可以看到值作为参数和引用作为参数两者是有比较大的差距的

值和引用作为返回值类型的性能比较

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


a40bcb14c39f4c9b93fe1d511e03487a.png

通过上面的比较,可以发现传值和引用再作为传参以及返回值类型上效率差距还是很大的。

C++引用VS指针

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



5271f26f6df74728ac24b442861714fd.png


底层实现上实际是有空间的,因为引用是按照指针的方式来实现的。


int main()
{
  int a = 10;
  //引用
  int& ra = a;
  ra = 20;
  //指针
  int* pa = &a;
  *pa = 20;
  return 0;
}

我们来看引用和指针的汇编代码对比>


01823f8f92664de1ae6151754e095827.png

引用和指针的不同>


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

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

3.没有NULL引用,但有NULL指针

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

5.引用自家即是引用的实体自加1,指针自加即是之怎向后便宜一个类型的大小

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

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

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

🐸内联函数🐸

什么是内联函数?

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


d0993a28fc3348cea4d3d9ab7a49da75.png

如果再Add函数前加上inline关键字将其改为内联函数,在编译期间编译器会用函数体替换函数调用。


查看是否优化的方式>


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

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


1d7be549ddb64f3ca336224754e697af.png


ffd1b72e47b34f7884a8670f4d9f554f.png

inline函数的特性

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


inline对于编译器而言只是一个建议,不同的编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数采用inline修饰,否则编译器会自动忽略inline特性,下图是《C++prime》第五版关于inline的建议:


de812664a9d4425d84543bf11e66ca87.png


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


//F.h头文件
#pragma once
#include<iostream>
using namespace std;
//声明
inline void f(int i);
//F.cpp文件
#include"F.h"
void f(int i)
{
  cout << i << endl;
}
//test.cpp文件
#include"F.h"
int main()
{
  f(10);
  return 0;
}


执行上面代码会出现链接错误>


326cd6a316374abcb321ae938f409f2d.png


这是因为声明时候告诉了编译器这是内联函数,那么编译器就会把f()函数展开,不会保存f()函数的地址,当调用main函数的时候会调用f()函数,就会去链接找f()函数的地址,而编译器认为f()函数是内联函数不会保存地址,所以才会出现链接不上的问题。

正确的写法应该是这样>


//F.h头文件
#pragma once
#include<iostream>
using namespace std;
//inline void f(int i);
inline void f(int i)
{
  cout << i << endl;
}
//test.cpp文件
#include"F.h"
int main()
{
    f(10);
    return 0;
}


将声明定义写在一起 这样就不会出现链接错误的问题了


b6323326ec7b455592519bac5d1b10e6.png

😺auto关键字(C++11)😺

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。


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


int testAuto()
{
  return 10;
}
int main()
{
  int a = 1;
  auto b = a;
  auto c = 'a';
  auto d = 1 + 1.1;
  auto e = testAuto();
  //typeid(变量名称).name()可以用来打印变量的类型
  cout << typeid(b).name() << endl;
  cout << typeid(c).name() << endl;
  cout << typeid(d).name() << endl;
  cout << typeid(e).name() << endl;
  return 0;
}


0420af352bcd4f0caeb3debebe2d0355.png


可以看到编译器可以自己根据=右边的值来推到出变量的类型。


auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化

【注意】


使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种"类型"的声明,而是一个类型声明时的"占位符",编译器在编译期会将auto替换为变量实际的类型。


auto的使用规则

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

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

在同一行定义多个变量

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

void testAuto()
{
    auto a = 1,b = 2;
    autp c = 3,d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}


auto不能推到的场景

  1. auto不能作为函数的参数
//编译报错,auto不能作为形参类型,因为编译器不能对a的实际类型进行推导
void testAuto(auto a)
{}
  1. auto不能直接用来声明数组
  2. 为了避免C++98中auto发生混淆,C++11中只保留了auto作为类型指示符的用法。
  3. auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。


😃基于范围的for循环C++11😃

范围for的语法

在C++98中如果要遍历一个数组,需要这样进行:

int main()
{
  int a[] = { 1,2,3,4,5,6 };
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
    cout << *(a + i)<<" ";
  }
  printf("\n");
  return 0;
}

对于一个有范围的集合而言,由程序员来说明循环范围是多余的,有时候还会容易犯错误,因此C++11中引入了基于范围的for循环,for循环后的括号由冒号":"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

int main()
{
  int a[] = { 1,2,3,4,5,6 };
  for (auto e : a)
  {
    cout << e << " ";
  }
  printf("\n");
  return 0;
}

【注意】:与普通循环累死,也可以用continue来结束本次循环,也可以用break来跳出整个循环。


范围for的使用条件

佛如循环迭代的范围必须是确定的

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


void testFor(char* a)
{
    for(auto e:a)
        cout<<e<<endl;
}
  1. ⚠这段代码是有问题的,因为for的范围不确定
  2. 迭代的对象要实现++和==的操作。(迭代会在以后的发文中为大家讲解)


🙈指针空置nullptr(C++11)🙈

C++98中的指针空值

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(NULL)调用指针版的f(int*)函数来看看结果>


02d2fbb86bbc44b6946a64a37537cf90.png


我们可以发现NULL被定义为0,反而调用了f(int*)函数。


在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制转换(void*)0.


【注意】:


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

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

3.为了提高代码的强壮性,在后续表示指针控制时建议最好使用nullptr。


🍀小结🍀

今天我们认识了C++引用、内联函数、auto关键字、nullptr相信大家看完有一定的收获。


种一棵树的最好时间是十年前,其次是现在! 把握好当下,合理利用时间努力奋斗,相信大家一定会实现自己的目标!加油!创作不易,辛苦各位小伙伴们动动小手,三连一波💕💕~~~,本文中也有不足之处,欢迎各位随时私信点评指正!

相关文章
|
2月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
24 0
|
2月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
34 0
|
2月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
2月前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
14天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
20天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
54 5
|
26天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
56 4
|
27天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
65 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4