目录
引用
引用概念
引用特性
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体.
常引用
使用场景
1.作为参数使用
2.作为返回值使用
引用和指针的区别
内联函数
内联函数的概念
内联函数特性
宏的优缺点
auto关键字
auto简介
auto使用的细则
auto不能使用的场景
基于范围的for循环
语法
使用条件
指针空值nullptr
C++98中的指针空值
引用
引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间.
一个人可以有多种称呼
比如:李逵 ,在家被称为“铁牛”,在江湖上被称为:”黑旋风“.
但无论哪一种称呼都是指李逵本身这个人.
用法如下:
类型& 引用变量名 = 引用实体
举个例子:
输入以下代码:
#include<iostream> using namespace std; int main() { int a = 5; int& ra = a;//对a取了个别名 ra,它们公用一块内存空间 cout << &a << endl;//输出a的地址 cout << &ra << endl;//输出b的地址 return 0; }
注意:引用类型必须和实体类型是同种类型的.
我们看输出结果:
它们的地址一样,说明它们确实指向了同一块内存空间,内容相同.
所以说如果改变ra的值,a的值也会随之改变.
引用特性
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体.
我们逐个来解释说明.
1.引用在定义时必须初始化
我们平常定义变量的时候,例如int a;char c;int* p...等等,都可以不用初始化,就是说不用给初值,但是引用初始化必须给初值.
int main()
{
int a = 5;
int& ra = a;
int& rb;//错误,没有给初值
return 0;
}
2.一个变量可以有多个引用
意思是一个人可以有多个外号,就像上面举的那个例子.
变量也是同样的道理,一个变量可以有多个别名.
int main()
{
int a = 5;
int& ra = a;
int& rb = a;
int& rc = a;
cout << ra << endl;
cout << rb << endl;
cout << rc << endl;
return 0;
}
输出结果如下:
3.引用一旦引用一个实体,再不能引用其他实体.
这个意思是比如你给李逵起了一个黑旋风的外号,这个外号以后就只能属于李逵他自己了,不能再把黑旋风这个外号给别人.
例如有两个变量a和变量b,我们给a起一个别名是ra,这个时候你就不能再把ra这个外号给b了.
int main()
{
int a = 5;
int b = 10;
int& ra = a;
ra = b;//千万注意!这里不是将b变成a的别名ra,而是将ra的值即a赋值为b.也就是说别名没有变,依然是a的别名,但是a(和别名)的内容变化了,变成了b
&ra = b;//按道理来说,这样才是修改a的别名,但这样并无法编译,所以无法修改别名
return 0;
}
常引用
这里会涉及一些权限的平移、放大与缩小问题.
我们知道,被const修饰的变量不可以被修改,相当于变成了只读权限了,不可以被写了.相当于权限变小了.
而我们平常不被const修饰的变量,既可以被修改,也可以被读取。所以它的权限比较大.
权限只可以被缩小和平移,不可以被放大!!!
这里还需要补充一点:在我们发生类型转化的时候,比如 int b = 0;double a = b;
编译器会先产生b的一份临时拷贝tmp(通过整型提升,类型为double),而临时常量具有常性,相当于tmp的类型为const double,再将tmp的值赋值给a.
int main()
{
int a = 5;
int& ra = a;//a的类型为int,ra的类型也为int,权限没有变化,可以平移,所以没有问题
const int b = 10;
//int& rb = &b;//错误b的类型为const int,而rb的类型为int,由于int权限大于const int,权限不可以被放大,所以错误
const int& rb = b;//正确,此时rb的类型也为const int,权限可以平移
int d = 15;
double& rd = d;//错误,d先产生一份const double类型的临时变量,由于double权限 > const double,所以权限放大,错误
const double& rdd = d;//正确
return 0;
}
使用场景
1.作为参数使用
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
因为参数是引用,所以他就是相当于实参的别名,所以如果交换,就切切实实交换了两个实参的值,而不是形参.
2.作为返回值使用
传引用返回:实质上是返回返回对象的别名
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
对于静态或全局变量,返回值可以用引用作为返回值,直接返回它本身,而不用再产生一份拷贝了.
但若不是全局变量会出现什么问题呢?
int& Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
}
先来看结果:
有的同学就说了,这是1啊,没有问题啊.
其实这是一种侥幸,进入函数之后,n++,此时n变成1.返回n的别名给ret
但仔细想一下,当把n的别名给ret的时候,是不是函数已经结束了!函数结束是不是临时变量就被回收了!
但是为什么结果是1呢?
因为此时编译器还没有清理或者修改增加别的新的内容. 里面的内容依然是1.
我们拿个例子来解释一下:你去住酒店,要住一晚上,然后第二天走了之后发现你的钱包和一些东西落在里面了,这个时候你赶紧返回去拿,发现钱包什么的都还在,你就拿到了它.这就是侥幸.
如果其中来了其他人或者保洁阿姨清理了这些东西呢?这个时候你也就拿不到了,所以说刚才编译器那次拿到这个1也是“侥幸”.
那么这次就没这么”侥幸“了.
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;
}
我们来看输出结果:
诶?ret不是Add(1,2),是1和2相加结果不应该是3吗?怎么会是7呢?
这次就没这么侥幸了.
首先第一次Add(1,2)返回了3,即把这个3留在了酒店.但此时3的主人已经走了.
后面又来了一个Add(3,4)即7,替代了这个3
这个时候3的主人回来再取,取到的已经不是原来它的东西了,只能是7了.这就造成了错误.
所以注意:
注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
引用和指针的区别
在语法概念上,引用就是一个别名,没有独立空间。和其引用实体公用一块空间.
在底层实现上,引用其实是有空间的,因为引用是按照指针的方式来实现的.
⭐引用和指针不同点:
1.引用在定义时必须初始化,指针没有要求
2.引用在初始化时引用一个实体后,就不能再引用其它实体,而指针可以在任何时候指向任何同一个类型的实体.
3.没有NULL引用,但有NULL指针
4.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占的字节个数(32位平台下占4个字节)
5.引用自加即引用的实体加1,指针自加即指针向后偏移一个类型的大小.
6.有多级指针,但没有多级引用.
7.访问实体方式不同,指针需要显式解引用,引用则编译器自己处理.
8.引用比指针使用起来更加安全.
内联函数
内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,从而不会有函数压栈的开销,提高运行效率.
看下图:
在Debug模式下,我们转到反汇编来看一下
可以看到call这个命令,这个其实就是在调用函数,说明此时并没有展开.
在Release模式下我们再试一下:
可以发现Add函数被展开了,并没有call函数.直接进行相加操作了.
内联函数特性
1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数
2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。(这条很重要,意味着你写inline但编译器不一定会展开,会根据代码的长短进行决断)
3. 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;
}
/// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
所以最好声明和定义写在一起.
宏的优缺点
优点:1.提高代码的复用性
2.提高性能
缺点:
1.不方便调试(预编译阶段进行了替换)
2.使代码可读性变差,可维护性差,容易误用
3.没有安全类型检查
C++有哪些技术可以替换宏?
1. 常量定义 换用const
2. 函数定义 换用内联函数
auto关键字
auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
总而言之就是:auto会自动推导变量的类型,而不用自己手动去写.通常类型名较长的时候用auto替代(或者自己也不知道变量是什么类型(doge))
看以下代码:
int main()
{
auto a = 1;
auto b = 1.5;
auto c = 'c';
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
运行结果如下:
可以看到auto已经成功推导出来了变量的类型.
需要注意的是:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
auto使用的细则
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
可以看到,无论auto加不加*,对于指针类型,结果都是一样的.
但引用必须加上&
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
可以看到同一行类型不同并不能编译通过.
auto不能使用的场景
1.auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2. auto不能直接用来声明数组
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
基于范围的for循环
语法
如果想要遍历一个数组,我们有以下两种方式
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此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 << " ";
return 0;
}
是不是非常简便.
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
使用条件
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
2. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后我会说明,现在大家了解一下就可以了)
指针空值nullptr
C++98中的指针空值
在良好的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)
{
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*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
需要注意的是:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的.
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr.