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.引用特性
-
引用在定义时必须初始化
-
一个变量可以有多个引用
-
引用一旦引用一个实体,再不能引用其他实体
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.特性
inline
是一种以空间换时间的做法,省去调用函数额外开销。所以代码很长或者有循环或者递归的函数不适宜使用作为内联函数 .inline
对于编译器而言只是一个建议,如果定义为inline
的函数体内有循环/递归等,编译器会自动优化,并忽略掉内联.
比如下面情况,就是说的上面两种特性:
int accumulate(int n)
{
int ans = 0;
for(int i = 1;i<=n;i++)
{
ans += i;
}
return ans;
}
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[] = {4,5,6};
}
基于范围的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
注意事项:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
- 在C++11中,
sizeof(nullptr) 与 sizeof((void*)0)
所占的字节数相同
以上是关于C++入门篇之引用,内联函数,auto和范围遍历的主要内容,如果未能解决你的问题,请参考以下文章