资讯专栏INFORMATION COLUMN

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

wangtdgoodluck / 1935人阅读

前言

承接上文入门篇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 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 #include 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 #include 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 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 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)所占的字节数相同

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/122192.html

相关文章

  • C++】初窥门径---入门

    摘要:在大型的工程中,自己定义的变量函数,类名与其他人定义的相冲突等问题。使用标准输出控制台和标准输入键盘时,必须包含头文件以及标准命名空间。缺省参数概念缺省参数是声明或定义函数时为函数的参数指定一个默认值。 目录 前言 1.命名空间 1.1命名空间定义 1.2 命名空间使用 2. C++的输入和...

    不知名网友 评论0 收藏0
  • C++C++快速入门

    摘要:中包含的即为命名空间的成员。使用输入输出更方便,不需增加数据格式控制,比如整形,字符可以连续输出,表示换行缺省参数备胎,就是给汽车准备一个备用轮胎,一旦那个轮子爆胎或者出了问题,备用轮胎就方便及时地取而代之,汽车就不至于中途抛锚。 ...

    TalkingData 评论0 收藏0
  • 秋招——语言(C++)

    摘要:语言不支持函数重载,编译后的代码只包含函数名。发布程序无需提供静态库,移植方便。全局和静态变量当且仅当对象首次用到时才进行构造。静态全局变量全局作用域文件作用域,无法在其他文件中使用。求数组数组名数据类型。 ...

    LuDongWei 评论0 收藏0
  • 系统地学习C++

    摘要:本书主要围绕一系列逐渐复杂的程序问题,以及用以解决这些问题的语言特性展开讲解。你不只学到的函数和结构,也会学习到它们的设计目的和基本原理。因此我们把精力集中在最有价值的地方。本书不仅是对模板的权威解释,而且本书还深入地介绍了其他一般的思想。 C++ 入门教程(41课时) - 阿里云大学 C+...

    joyqi 评论0 收藏0
  • C++从0到1】新手都能看懂的C++入门(上),建议收藏

    摘要:上面这三种均不造成重载,现在来说明原因。结论对于引用返回,返回的对象必须是栈帧销毁后还存在的。全局,静态,未销毁的函数栈帧当中的都是可以的指针与引用如图两者底层实现差不多,引用是用指针模拟的。不建议声明和定义分离,分离会导致链接错误。 ...

    xcold 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<