C++入门篇之引用,内联函数,auto和范围遍历

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++入门篇之引用,内联函数,auto和范围遍历相关的知识,希望对你有一定的参考价值。

前言

承接上文入门篇1,博主这次将会继续更新以下内容:extern ,引用 ,内联, auto ,范围for循环 和 C++中的空指针表示法(温馨提示:都是讲解浅显的知识,后面会深入讲解.),浏览此文,大家可以根据上面的目录进行定位哦~~

extern “C”

我们知道,在c语言中就只能编译c写的程序,但是在C++中却可以完全兼容c程序,其中缘由就是对于程序的名字修饰规则不同请看这里.也就是说c无法用c++写的函数方法,但实际情况中,我们是会需要用到一些通过c++编写的函数库,为了达到此目的,并且编译器可以顺序编译,便引入了此语句extern "C",将它放在函数声明前,便是告诉编译器要使用c的风格进行编译,以完成c也可以用c++编写的函数库.

比如谷歌用c++写的tcmalloc库中就会提供tcmalloc()和tcfree()来代替c中的malloc()和free()进行提高效率,但是在c中无法使用tcmalloc,为了解决此问题,我们在要是用的函数前加上extern C就行.

例子:

extern "C" int add(int a,int b);

int add(int a,int b)
{
    return a+b;
}

小结: 其实该用法更多是在c++中,可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,
将该函数按照C语言规则来编译.总之,加上这句话后,无论是c还是c++都可以进行编译


引用

1.概念

引用的概念不同于指针,引用只是起别名,并未开多的空间,也未创造新东西.比如孙悟空,他可以叫美猴王,但是也有齐天大圣的称号,而这几个称号都是同一个对象.我们如果说:“美猴王喜欢姑娘”,那么也就是"齐天大圣喜欢姑娘",本质是一个对象.


2.语法

类型& 引用变量名(对象名) = 引用实体; (引用实体的类型必须和&前的类型保持一致哦~~)

int main()
{
    int a = 10;  //a相当于就是孙悟空
    int& b = a;  //b相当于是美猴王
    int& c = a;  //c相当于是齐天大圣
    
    b = 100;     //b等于100,相当于美猴王喜欢姑娘(100),那么理所应当的,齐天大圣和孙悟空都会变成100
    
    cout <<"a的值为:"<<a<<endl;
    cout <<"b的值为:"<<b<<endl;
    cout <<"c的值为:"<<c<<endl;
    return 0;
}


3.引用特性

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

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

  3. 引用一旦引用一个实体,再不能引用其他实体

int main()
{
    int a = 10;  
    
    int d = 1000;
    
    int& b = a;  //一旦写了引用,就必须有完整的实体,不能写成  int& b;  这是不允许的,即第一条特性
    int& c = a;  //a变量被引用了两次,也就是第二条特性意思
    
    c = d; //这里不再是c是d的别名,而是c变成了1000,因为c已经成了a的别名,那么c就永远只能是a的别名.   第三条特性意思

    return 0;
}

测试题:

int x = 0,y = 1;
int* p1 = &x;
int* p2 = &y;
int*& p3 = p1;
/*********************************************************/
*p3 = 10;
p3 = p2;

在分割线以前的图是如下:

请你画出分割线以后的图:

?

?

答案:

大家仔细想想为何p3还在p1那里,想想引用的特性哦~~~


4.常量引用

对于常数来说,无法直接引用,需要使用const,因此叫做常引用,如下:

int a = 10;
int& a1 = a;   //正常引用,没问题

const int b = 10;
int& a2 = b;   //引用失败,因为b是常数,无法int引用
const int& a3 = b;  //成功引用;

int& a4 = 100; //引用失败,因为100是常数,无法int引用
const int& a5 = 100;  //成功引用

所以,有人总结出想使用引用的条件是:可以缩小读写权限,但不能放大读写权限.

根据以上特性,在实际运用中,引用一般有什么意义呢?

答曰:

  • 函数传参时,可以减少传参拷贝(引用作用)
  • 函数传参时,可以保护形参不被修改(常量引用作用)
  • 函数传参时,既可以接收变量,又可以接收常量(常量引用作用).

针对特性一例子:

struct node   //某个结构体定义如下:
{
    int val;
    char left;
    int right;
    struct node* next;
};

void modify(struct node& node0)  //某函数定义如下:  如果其参数设置为引用,将不需要通过函数传递方式中的值传递(拷贝),造成空间消耗巨大.
{
    //此处省略相关操作....
}

int main()
{
    struct node Node;
    modify(Node);
    return 0;
}

针对特性二和特性三例子:

如果一个函数在执行相关操作中,只是需要访问参数的值,并不需要修改参数,那么可以用常量引用.

int add(const int& a,const int& b)
{
    return a+b;  //比如加法函数,如果手误,码码错代码,修改了a或b的值,编译器会自动提示.
}

int main()
{
    int a = 10;
    int b = 20;
    cout<<"变量作为实参"<<add(a,b)<<endl;
    cout<<"常量作为实参"<<add(10,20)<<endl;   //如果函数形参不写成引用,将无法接收常量.
    return 0;
}

5.引用做函数返回值

当引用做函数返回值时候,返回的是一个指向返回值的隐私指针.这样,函数就可以放在赋值语句的左边。例如,请看下面这个简单的程序:

#include <iostream>
using namespace std;
double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0};
double& setValues(int i) 
{  
   double& ref = vals[i];    
   return ref;   // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i]
}


// 要调用上面定义函数的主函数
int main ()
{
   cout << "改变前的值" << endl;
   for ( int i = 0; i < 5; i++ )
   {
       cout << "vals[" << i << "] = "<< vals[i] << endl;
   }
    
   setValues(1) = 20.23; // 改变第 2 个元素
   setValues(3) = 70.8;  // 改变第 4 个元素
    
    
   cout << "改变后的值" << endl;
   for ( int i = 0; i < 5; i++ )
   {
		cout << "vals[" << i << "] = "<< vals[i] << endl;
   }
   return 0;
}

结果:

注意点:

当引用作为函数返回值时,被引用的对象其作用域必须是有效范围,所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用.

int& func() {
   int a = 100;
   // return a; // 错误的引用返回
   static int x;
   return x;     // 安全,x 在函数作用域外依然是有效的
}

6.引用注意点

我们看下面这个例子:

int i = 10;
double b = i;         //可以编译成功
double& c = i;        //报错,因为引用实体类型和引用类型必须一致
const double& d = i;  //成功,原因是 i到 d过程中,会先产生一个临时空间,然后把i的值放到临时空间中,又由于临时空间具有常性,所以加上const就成功

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

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

#include <time.h>
#include <iostream>
using namespace std;
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}

int main()
{
	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;
	return 0;
}

#include <time.h>
#include <iostream>
using namespace std;
struct A { int a[10000]; };

A a;
A TestFunc1() { return a; }
A& TestFunc2() { return a; }

int main()
{
	// 以值作为函数返回值
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函返回值
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "A TestFunc1()-time:" << end1 - begin1 << endl;
	cout << "A& TestFunc2()-time:" << end2 - begin2 << endl;
	return 0;
}

通过上述代码的比较,发现传值和引用在作为传参以及返回值类型上效率相差很大


内联函数

1.概念

一种通过inline修饰的函数,编译器进行编译时可以直接在函数调用的地方进行展开,不需要多余的函数栈帧开销,节约了时间

普通函数调用:

通过右边的汇编代码可以看到,调用add函数需要call命令,说明消耗了栈帧空间.


内联函数调用

可以用过右边的汇编代码看到,调用add函数时候,是直接展开add内容进行使用的,并未进行专门的函数调用.


因此,内联函数可以提升效率.其实本质上来说,C++的内联函数特性就是为了解决C语言中 宏 的书写麻烦.也就是说,内联的出现是为了替代宏.


2.特性

  1. inline是一种以空间换时间的做法,省去调用函数额外开销。所以代码很长或者有循环或者递归的函数不适宜使用作为内联函数 .
  2. inline对于编译器而言只是一个建议,如果定义为inline的函数体内有循环/递归等,编译器会自动优化,并忽略掉内联.

比如下面情况,就是说的上面两种特性:

int accumulate(int n)
{
    int ans = 0;
    for(int i = 1;i<=n;i++)
    {
        ans += i;
    }
    return  ans;
}
  1. inline建议声明和定义不可分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到.

比如下面这种情况:

// F.h    头文件的内容
#include <iostream>
using namespace std;
inline void f(int i);

// F.cpp  源文件的内容
#include "F.h"
void f(int i)
{
	cout << i << endl;
}

// main.cpp
#include "F.h"
int main()
{
	f(10);
	return 0;
}

此时编译器便会显示链接错误


auto关键字

1.概念

一个新的类型指示符,auto声明的变量必须由编译器在编译时期推导而得.

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int b = 20;
    double c = 12.12;
    double d = 12.13;
    
    auto e = a + b;      //e 的类型是int,编译器会自行推导
    auto f = c + d;      //f 的类型是double,编译器会自行推导
    return 0;
}

2.auto的使用细则

auto与指针和引用结合起来使用:
用auto声明指针类型时,auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int main()
{
	int x = 10;
	auto a = &x;          //auto的类型是 int*
	auto* b = &x;         //auto的类型是 int*
	auto& c = x;          //auto的类型是 int
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
    *a = 20;
	*b = 30;
	 c = 40;
	return 0;
}

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

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

3.auto不能推导的场景

auto不能作为函数的参数,不能直接用来声明数组.

void TestAuto(auto c)
{
	int a[] = {1,2,3};
	auto b[] = {456};
}

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

在C++98中如果要遍历一个数组,可以按照以下方式进行:

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
		cout << array[i]<<' ';
}

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

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)
    	e *= 2;          //通过引用,改变值
    for(auto e : array)
    	cout << e << " ";  //挨个输出
}

使用条件

范围for的使用条件,必须确定明确范围

  • 对于数组而言,就是数组中第一个元素和最后一个元素的范围
  • 对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
void TestFor(int array[])   //这种接收方式,本质上是指针,所以下面的范围遍历便不适用,因为没有明确的范围标志.
{
	for(auto& e : array)
		cout<< e <<endl;
}


指针空值nullptr

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
	int* p1 = NULL;
	int* p2 = 0;
}

而NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

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

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

void f(int x)
{
	cout<<"f(int)"<<endl;
}
void f(int* x)
{
	cout<<"f(int*)"<<endl;
}

int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);       //大家现在猜猜结果会是啥?
	return 0;
}
																 ![image-20211009204652154](https://img-blog.csdnimg.cn/img_convert/702a3c6fbdfa5bafe397fe8f7dc085da.png)

惊不惊喜,意不意外?我们传参NULL时候,本意是想调用第二个函数,但是编译器却认为我们想要调用第一个函数,这就是在C语言中使用NULL的缺陷,因此,C++提出了nullptr代替NULL

注意事项:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
  2. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
  3. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同

以上是关于C++入门篇之引用,内联函数,auto和范围遍历的主要内容,如果未能解决你的问题,请参考以下文章

C++入门篇引用&&内联函数&&auto&&范围for&&nullptr

C++基础入门知识整理与总结

C++基础入门知识整理与总结

C++基础入门知识整理与总结

C++入门

C++ 初阶 新语言入门介绍:命名空间,(全/半)缺省函数,函数重载,引用,内联,auto