【C++初阶(三)】引用内联函数auto关键字
目录
前言
1. 引用
1.1 引用的概念
1.2 引用的特性
1.3 引用的权限
1.4 引用的使用
1.5 引用与指针的区别
2. 内联函数
2.1 什么是内联函数
2.2 内联函数的特性
3. auto关键字
3.1 auto简介
3.2 auto使用规则
3.3 auto不能使用的场景
4. 基于范围的for循环
4.1 范围for使用
4.2 使用条件
5. C++空指针
总结
前言
在学习C语言时,大家或许都被指针为难过,在使用指针时也存在各种问题,比如:空指针野指针问题(指针可以在任何时候指向任何地址,包括无效地址)。此外在C语言中函数调用时,如果多次的调用同一函数,创建大量的函数栈帧就会导致性能下降,对于这些缺点,C++都进行了优化与改进。那么本期的 “主角” 就是引用&内联函数。
1. 引用
1.1 引用的概念
引用是C++中的一个重要概念,它是一个已存在变量的别名,引用不是新定义一个变量,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。对引用的操作实际上就是对原变量的操作。
引用的定义方式为:类型& 引用名 = 原变量名;
比如:
int main()
{
int a = 1;
int& b = a;
a++;
b++;//b++也就是a++
cout << a << endl;//输出3
return 0;
}
注意:引用类型必须和引用实体是同种类型的。
1.2 引用的特性
- 引用必须初始化
- 不占用额外空间(它引用的变量共用同一块内存空间)
- 不能修改引用的绑定(引用一旦引用一个实体,就不能再引用其他实体)
- 引用可作为函数参数和返回值
int main()
{
int a = 1;
int b = a;
//int& t;出现报错
int& c = a;
int& d = a;
int& e = c;
//地址相同
cout << &a << endl;
cout << &c << endl;
cout << &d << endl;
cout << &e << endl;
return 0;
}
1.3 引用的权限
在C++中,引用的权限与指针类似,可以分为两种权限:常量引用和非常量引用。
常量引用:使用const修饰的引用被称为常量引用。常量引用只能读取被引用变量的值,不能修改被引用变量的值。
如果引用实体使用const修饰,那引用也必须使用const修饰。也就是权限可以被缩小,但不可以被放大。
比如:
int main()
{
const int a = 1;
//int& b = a;出现报错,权限被放大
//权限要等大
const int a = 1;
const int& b = a;
//权限可以缩小
int c = 2;
const int& d = c;
return 0;
}
除此之外,在类型转换时创建的临时变量也具有常属性。
int main()
{
int i = 1;
double j = i;
//double& rj = i;//报错
//类型转换过程中
//存在隐式转换产生临时变量,这里的临时变量具有常性
const double& rj = i;
return 0;
}
1.4 引用的使用
做参数
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
和指针相比
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
使用上更加简洁,并且引用的传值效率也很高,比如我们传值时传一个较大的数组,传值调用将一个很大内存的数组转换为数组地址传过去(变为4字节)
struct A{ int a[10000]; };
void Func1(A a){}
void Func2(A& a){}
这些指针可以完成的工作,引用也可以完成。
做返回值
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
在之前我们先来理解一下传值返回:
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
这里返回的n是Count函数里的n吗?答案不是,在C语言函数中,函数执行结束函数内创建的变量就会销毁,所以这里返回的是Count()函数中变量n数值的拷贝(返回的是n的值(临时变量),而不是n这个变量)。
int& Count()
{
int n = 0;
n++;
return n;
}
如果使用引用做返回值,返回的就是n的别名,也可以理解为返回的就是n这个变量(会报警告:返回局部变量或临时变量的地址: n)。这样直接返回函数内变量是很危险的,因为函数内的n出了函数作用域就被销毁了,再返回原本的n地址处的数据,此时数据是不可控的。
int main()
{
int& ret = Count();
cout << ret << endl;//第一次输出还是正常值1
cout << ret << endl;//第二次输出就变成随机值了
}
1.5 引用与指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
int main()
{
int a = 10;
int& ra = a;
cout << "&a = " << &a << endl;//输出的地址相同
cout << "&ra = " << &ra << endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
下边是引用与指针底层汇编的对比:
引用与指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节) - 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
2. 内联函数
2.1 什么是内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
如上图,函数在调用时并没有创建函数栈帧,而是直接在main函数内部展开。
2.2 内联函数的特性
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
缺陷:可能会使目标文件变大,
优势:少了调用开销,提高程序运行效率.
但也并不是所有的函数都要展开,内联函数展开的也只适用于较小的函数。
比如:
一个函数有100行代码,被调用了1000次,每次都展开,那就是10w行代码,产生的文件要多大,而如果是函数栈帧调用,每次调用都去同一块空间调用函数,也就只多了100行代码。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性
内联函数只是向编译器发送一个请求,编译器可以选择忽略
- 声明和定义分离时不可以使用内联函数
分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
内联函数在声明和定义分离时,直接调用会出现报错,但是可以间接调用。
//fun.h
inline void fun()
{
cout << "hello ,world!" << endl;
}
//fun.c
void func()
{
fun();
}
//test.c
int main()
{
fun();
return 0;
}
这种情况是可以正常调用的。
3. auto关键字
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
3.1 auto简介
使用auto修饰的变量,是具有自动存储器的局部变量 ,但是在日常中却很少使用。
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
比如:
int Test()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = Test();
cout << typeid(b).name() << endl;//int
cout << typeid(c).name() << endl;//char
cout << typeid(d).name() << endl;//int
return 0;
}
auto会自动读取类型,并且使用auto定义变量时必须对其进行初始化
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译器会将auto替换为变量实际的类型
3.2 auto使用规则
- 与指针引用结合使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;//int*
cout << typeid(b).name() << endl;//int*
cout << typeid(c).name() << endl;//int
return 0;
}
- 一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化类型不同
3.3 auto不能使用的场景
- auto不能作为函数返回类型
最新的C++语法是可以的,但是很不推荐,在日常应用场景中一定不要这样写,在较大的项目中大多数都是多人合作的,如果你使用auto,那别人在调用你的函数接口时不知道返回类型很难搞,如果别人也用auto,这样就会导致一系列连锁,最后代码越写越 “ 屎山 ”,这就是典型的 “ 坑队友 ”,要养成良好的代码风格。
- auto不能作为函数的参数
void TestAuto(auto a)//出现报错,编译器无法对a的实际类型进行推导
{
}
int main()
{
TestAuto(2);
}
- auto不能直接声明数组
4. 基于范围的for循环
4.1 范围for使用
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
//正常的for循环遍历
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
cout << arr[i] << ' ';
}
cout << endl;
//范围for遍历
for (int e : arr)
{
cout << e << ' ';//这里的e是数组值的拷贝,改变e无法改变数组的数据
}
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
注意:范围for与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
4.2 使用条件
- for循环迭代的范围必须是确定的
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
例如上述代码,范围并不明确。对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
5. C++空指针
在C语言中我们常用的都是NULL来代表空指针,NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何
种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦 。我们可以使用C++代码测试一下:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0); //f(int)
f(NULL); //f(int)
f((int*)NULL); //f(int*)
return 0;
}
不难发现NULL在C++中被替换成了0,在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
为了保险起见,C++引入nullptr,代表空指针,在后续表示指针空值时建议最好使用nullptr。
总结
到本期C++的一些入门简单语法已经基本介绍完毕,后续将会继续深入学习C++,以上便是本期全部内容 ,最后,感谢阅读!