目录
一、命名空间
1.C的命名缺陷
2.域和命名空间
3.命名空间的使用与嵌套
二、输入输出
三、缺省参数(默认参数)
1.缺省参数的概念
2.缺省参数分类
1>全缺省参数
2>半缺省参数
3.缺省参数的一些使用场景
4.缺省参数在分文件使用的注意事项
四、函数重载
1.函数重载的概念
2.函数重载与缺省参数
3.为什么C语言不支持,C++支持重载,C++是怎么支持重载的?
1>编译链接过程
2>函数名修饰规则
3>如果可以修改函数名修饰规则,那么修改规则使得参数相同,函数名相同,但返回类型不同的函数构成重载?
4>一定存在链接阶段吗
五、引用
1.引用的概念
2.引用的使用
3.引用特性
4.常引用
5.引用的使用场景
1>做参数
2>引用做返回值
6.引用和指针的区别
七、内联函数
1.概念
2.使用
3.内联函数的注意事项
八、auto(c++11)
九、基于范围的for循环(C++11)
十、指针空值nullptr(C++11)
一、命名空间
1.C的命名缺陷
假设我们用c语言写了一段代码
#include<stdio.h> int rand = 0; int main() { printf("%d\n", rand); return 0; }
正常情况下,都会打印0
但是我们有时候会包stdlib这个头文件,我们发现报错了
根据上面的提示,我们不难发现,这是因为rand在stdlib这个头文件中已经被使用了。所以发生了重定义现象。
试想一下,一旦使用c语言去写一些大型项目,那么毫无疑问,会出现大量的重定义现象。
由此我们引入了namespace这个关键词,他的意思是命名空间
2.域和命名空间
我们在c语言的时候就了解过作用域的概念,比如下面的代码中,我们最后打印出来的结果是1,他是可以编译通过的。而打印出main函数里面定义的a的原因是因为局部优先。这个我们也称作局部域,而全局的a则位于全局域
#include<stdio.h> #include<stdlib.h> int a = 0; int main() { int a = 1; printf("%d\n", a); return 0; }
我们的域可以分为类域,命名空间域,全局域,局部域。
有局部域,优先访问局部域,没有局部,优先访问全局的。
当局部域和全局域都不存在的时候且命名空间域存在,直接输出a是无法输出的,也就是说无法直接访问命名空间域
上面的是局部域不存在的情况,我们现在来讨论一下如果全局域和局部域同时存在的话,如何访问全局域?
c语言是无法实现这个的。除非使用指针等。
而在c++中为了实现这个,引入了::操作符,这个操作符的作用是域作用限定符,他的使用如下代码所示,即在a的前面使用::,在左边可以加上一个空格(当然不加也无所谓),我们代表从全局域中去访问
#include<stdio.h> #include<stdlib.h> int a = 0; int main() { int a = 1; printf("%d\n", ::a); printf("%d\n", a); return 0; }
也就是说,通过这个操作符,我们可以实现无论是否有局部域,都可以去访问全局域
既然全局域的访问解决了,我们前面也提到过,命名空间域无法直接访问。那么该如何访问呢,答案是展开了命名空间域或者指定访问。
#include<stdio.h> #include<stdlib.h> int a = 0; namespace boundary { int a = 2; } int main() { int a = 1; printf("%d\n", ::a); printf("%d\n", a); printf("%d\n", boundary::a); return 0; }
如上所示是指定访问的方式
如下是展开命名空间的访问方式
那么如果即存在全局域又展开了命名空间域
则会出现不明确的现象
而如果不展开的话,那么就直接访问全局域
其实到了这里,我们也许已经混乱了。
但是其实核心如下:
1.优先局部域,其次全局域,最后访问展开的命名空间域或者指定的命名空间域
展开了命名空间域其实就相当于将这个变量给暴露在全局中,获得了全局的特点
2.如果是指定访问,那么没有特别需要注意的点,唯一需要注意的是,展开了命名空间域以后,他将获得全局的特点,如果使用全局的指定方式,那么将他可以访问这个变量
3.如果是直接访问,这个比较复杂
先看是否存在局部域,如果有局部域,那么直接访问局部域
如果没有局部域,那么就要再看是否存在全局域和命名空间域。
如果有全局域但没有命名空间域,那么直接访问全局域
如果没有全局域但存在命名空间域,如果没有展开,直接访问是无法访问到的,如果展开,直接访问可以访问到
如果全局域和命名空间域均存在,那么我们就要判断命名空间域是否展开
如果没有展开,访问全局域,如果展开,相当于两个相同命名的变量暴力在全局中产生冲突。
其次局部域不可以展开命名空间
3.命名空间的使用与嵌套
1.命名空间可以使用变量/函数/结构体
namespace N0 { // 命名空间中可以定义变量/函数/类型 int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
2.命名空间可以嵌套
//2. 命名空间可以嵌套 namespace N1 { int a; int b; int Add(int left, int right) { return left + right; } namespace N2 { int c; int d; int Sub(int left, int right) { return left - right; } } }
3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。 // ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个 namespace N1 { int Mul(int left, int right) { return left * right; } }
如上述的三种代码的嵌套等引用操作如下所示
int main() { printf("%d\n", N0::rand); printf("%d\n", N0::Add(2, 3)); printf("%d\n", N1::N2::Sub(1, 2)); return 0; }
二、输入输出
c++的输出是使用cout的
但是这个cout是定义在c++的标准库中的标准命名空间的,直接使用会报错的,我们想要使用这个输出,我们有两种方式可以做到
一种方式是直接展开命名空间,如下所示
另一种方式是使用指定访问
在使用第一种方式的时候,我们直接展开可能会展开冲突,这是很危险的。所以在项目中一般不直接展开
但是我们还有一种方式,就是将常用的给展开
我们现在来理解一下这个<<这个操作符,这个操作符在c语言中是移位操作符,在c++里面他又有了流插入运算符的含义
意思就是将"hello world "这个字符串插入到流中,而且我们还需要注意的是endl其实就是'\n',换行的意思,就是流插入hello world之后,在插入一个换行,将换行插入流
而且流插入可以使用多个类型
我们也可以发现cout的特点就是可以自动识别类型。
还有一个是输入cin,以及>>流提取运算符
他们的使用如下
#include<iostream> //using namespace std; using std::cout; using std::endl; using std::cin; int main() { int x = 10; double y = 11.11; cout << x << " " << y << endl; cin >> x >> y; cout << x << " " << y << endl; std::cout << "hello world" << std::endl; }
需要注意的是,使用cin也许会出现精度缺失的现象,我们有时候也需要打印宽度和精度的数据,这里我们建议采用c语言的方法,而不采用c++的方法,因为c++的方法过于繁琐
而且printf和scanf是比c++的cout和cin快的。
三、缺省参数(默认参数)
1.缺省参数的概念
缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
例如下面的代码#include<iostream> using namespace std; void Func(int a = 0) { cout << a << endl; } int main() { Func(); //没有传参时,使用参数的默认值 Func(10);//有传参时,使用指定的值 return 0; }
2.缺省参数分类
1>全缺省参数
如下代码所示,每一个参数都缺省了。这就是全缺省参数。在全缺省参数的传参中,我们如果需要指定值,必须要从左到右指定。中间不可以跳过一个传参。一旦出现一个缺省的,后面的必须都不可以指定了
#include<iostream> using namespace std; void Func(int a = 10, int b = 20, int c = 30) { cout << a << ' '; cout << b << ' '; cout << c << ' '; cout << endl; } int main() { Func(); Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
比如说像下面这种是错误的
2>半缺省参数
如下代码所示,是半缺省的代码样例。在半缺省中,指得并非是缺省一半,而是有些是缺省,有些不是缺省。且缺省的参数必须是从右往左依次缺省的。不能跳着缺省。这样是为了保证传参的时候,必须先将未缺省的参数给传递,然后在传递缺省的参数。
且需要注意,下面这段代码存在一些问题,是调用函数的时候需要将未缺省的参数全部传递。不可以遗漏
#include<iostream> using namespace std; void Func(int a , int b , int c = 30) { cout << a << ' '; cout << b << ' '; cout << c << ' '; cout << endl; } int main() { Func(); Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
3.缺省参数的一些使用场景
如下代码所示,是在使用栈的使用使用缺省参数,在这段代码中,我们将栈的一个参数设置为了缺省参数。这样的好处就在于,未来我们明知道这块有1000个数据,我们就可以直接设置1000个初始空间。如果不知道的话,就跟正常的操作一样,默认初始容量为4
#include<iostream> #include<assert.h> using namespace std; typedef struct Stack { int* a; int top; int capacity; }Stack; void StackInit(Stack* ps, int defaultcapacity = 4) { assert(ps); ps->a = (int*)malloc(sizeof(int) * defaultcapacity); if (ps->a == NULL) { perror("malloc"); return; } ps->top = 0; ps->capacity = defaultcapacity; } int main() { Stack st; //知道需要多少个数据 StackInit(&st, 1000); Stack st1; //不知道需要多少个数据 StackInit(&st1); return 0; }
4.缺省参数在分文件使用的注意事项
如下图所示,对于如下图所示的代码,出现了缺省参数,在缺省参数的函数中,我们让声明和定义都缺省了。这样做其实是错误的。这是为了防止声明和定义给的缺省值不一样。如果不一样的话,会出现问题
那么既然不可以两个同时给,那么应该给声明还是定义呢?
答案其实是应该给声明。这是因为在预处理阶段,头文件需要展开。然后各自编译自己的文件。如果没有在声明中给出了缺省参数的话。那么在test.c文件中,他会认为,传参传的数量不正确。所以会报错。
由此我们必须得在声明函数中使用缺省参数。这样编译器就会自动的将StackInit(&st1)认为成StackInit(&st1,4)
四、函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前 者是 “ 谁也赢不了! ” ,后者是 “ 谁也赢不了! ”
1.函数重载的概念
函数重载: 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这些同名函数的 形参列表 ( 参数个数 或 类型 或 类型顺序 ) 不同 ,常用来处理实现功能类似数据类型不同的问题。注意:对返回值没有任何要求。如果函数名、参数个数、类型、类型顺序均相同,但返回值不同,并不构成重载,而是直接报错
1>参数类型不同如下代码所示,就是参数类型不同导致的重载#include<iostream> using namespace std; int Add(int x, int y) { return x + y; } double Add(double x, double y) { return x + y; } int main() { cout << Add(1, 2) << endl; cout << Add(1.1, 2.2) << endl; }
2>参数个数不同如下代码所示,是参数个数不同导致的重载#include<iostream> using namespace std; void Func(int a) { cout << "Func (int a)" << endl; } void Func() { cout << "Func()" << endl; } int main() { Func(5); Func(); }
3>参数顺序不同如下所示,是参数顺序不同导致的重载#include<iostream> using namespace std; void Func(int a, char b) { cout << "Func (int a,char b)" << endl; } void Func(char a, int b) { cout << "Func (char a, int b)" << endl; } int main() { Func(5, 'a'); Func('a', 7); }
在这里需要注意一点的是,是参数的类型不同,而非名字
如下代码所示,第一个函数和第三个函数并不构成重载
2.函数重载与缺省参数
我们看下面的代码
这段代码中,两个函数确实是构成重载的。编译不会报错。但是在无参调用的时候也会出现歧义而导致报错
3.为什么C语言不支持,C++支持重载,C++是怎么支持重载的?
1>编译链接过程
对于c/c++的编译链接过程,基本上是一样。没有太大的区别
对于c/c++语言,我们的vs首先要对其进行预处理预处理阶段需要做这些事情:头文件展开/宏替换/条件编译/去掉注释等等,最后他们会生成一个后缀为.i的文件比如说,我们的项目中有三个文件,Test.c,Stack.h,Stack.c三个文件经过预处理之后,他们就会生成Test.i和Stack.i这两个文件,但是要注意,这两个文件可不是直接覆盖原来的文件,他们是拷贝后经过处理后得到的。还有一点要注意的是,是由Test.c和Stack.h生成Test.i,由Stack.c和Stack.h生成Stack.i
在然后就进入编译阶段,在编译阶段是用来检查语法,生成汇编代码的最后就会生成Test.s,Stack.s这两个文件,这两个文件是分别由Test.i和Stack.i生成的
然后是汇编阶段:在汇编阶段,汇编代码将转化为二进制机器码他们的后缀是Stack.o和Test.o
最后这些机器码将转化为可执行程序.exe/a.out
2>函数名修饰规则
有了上面的编译链接过程,现在我们来着重分析汇编代码。我们使用如下程序我们直接跳转到反汇编阶段
我们需要注意的一条指令是call指令,他的作用是函数调用
也就是说我们的函数调用其实就是call 加一个地址,这个StackPush这个符号是可以没有的
当我们在反汇编中进行调试的时候,这个call指令会自动跳到jmp这条指令中
我们可以发现这个jmp指令的地址其实就是call后面的地址,这jmp指令也是一条跳转指令
继续调试,他会跳转到这里
综上所述
具体调用逻辑如下图所示
我们现在在来分析一下,编译阶段,也就是test.i变为test.s这个过程中,StackPush能否拿到地址?其实是不可以的,因为编译阶段只是包含了头文件,头文件中只是声明了一下。这里并没有地址地址其实是在Stack.i变成Stack.s这个过程中的。我们之前只有声明,虽然没有地址但是是可以通过的,而在Stack.i中才有函数的定义,这里面才会有一些push等各种指令,具体函数的实现,有地址。这时候因为文件分离了,并没有合在一起,所以test.c并没有地址。只有链接时候才会有地址这也正是由于链接阶段会有符号表的合并和重定位
那么在这里我们就产生疑问了,既然如果有两个相同的函数名的话,他们的符号就相同,是如何区分地址的呢?事实上,这就是c语言不支持重载的原因,c语言直接把函数名作为符号。而c++不会直接将函数名作为符号。c语言太直接了,c语言不需要区分,只要重名,直接报错就可以了
那么c++是如何支持的呢?如下所示是linux环境下,我们可以看到函数名被重新修饰了。这个4代表的是函数名的长度,后面的i和d是int和double的缩写。而返回类型并没有被用于重新修饰。要注意的是,这个只是g++的函数名修饰规则,每个编译器的规则都有所区别而对于栈的话,也是用类似的方式进行函数名修饰,这样的话,符号就不一样了。就可以支持重载了
3>如果可以修改函数名修饰规则,那么修改规则使得参数相同,函数名相同,但返回类型不同的函数构成重载?
先说结论:不可以这是因为函数名修饰规则发生在链接阶段而如果参数相同,函数名相同,返回类型不同的话,下面的代码就会产生歧义,直接在编译阶段就已经挂了
4>一定存在链接阶段吗
不一定。如果是分文件的话,那么声明和定义是分开的,需要链接。
但是也有可能声明和定义就在一块。定义就是声明。这时候不需要链接
五、引用
1.引用的概念
引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间。比如: 李逵 ,在家称为 " 铁牛 " ,江湖上人称 " 黑旋风 " 。
2.引用的使用
我们分析如下的代码
#include<iostream> using namespace std; int main() { int a = 0; int& b = a; int& c = b; int& d = c; cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; b++; c++; cout << a << ' ' << b << ' ' << c << ' ' << d << endl; return 0; }
运行结果如下
这是因为b是a 的别名,b和a共用一块空间,c是b的别名,d是c的别名
他们都是一块空间的,所以地址是一样的
b++,c++后,都是对这一块空间进行++。
所以最终打印出来的是2 2 2 2
还需要注意的是,引用一开始必须要给一个初始值。否则报错,因为后序无法说明这个是谁的别名,c++的引用是不可以进行修改的。
引用之后,直接修改d,是修改的这一块空间的值,而不是让d改为了x 的别名,这是一个赋值操作
引用还可以用来当作函数传参时候使用,如下代码所示#include<iostream> using namespace std; void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } int main() { int x = 0; int y = 1; Swap(x, y); cout << x << ' ' << y << endl; return 0; }
a是x的别名,b是y的别名。a就是x的这块空间,b也是y的这块空间。
那么我们就可以直接交换即可,就不需要传指针了
引用也可以使用指针来传参
#include<iostream> using namespace std; void Swap(int*& a, int*& b) { int* tmp = a; a = b; b = tmp; } int main() { int x = 0; int y = 1; int* px = &x; int* py = &y; cout << px << ' ' << py << endl; Swap(px, py); cout << px << ' ' << py << endl; return 0; }
这段代码运行结果为
int*& a是px的别名,int*& b是py的别名
我们直接交换a和b这两个别名就可以了,需要注意的是,我们将a这个别名的值存放到一个临时空间的时候,我们需要使用int* 类型的临时变量来接收。这是因为别名a的这块空间是int*类型的
还有一种情形是这样的,在使用单链表的尾插时候
#include<iostream> using namespace std; typedef struct ListNode { int val; struct ListNode* next; }ListNode, * PListNode; void ListPushBack(ListNode*& phead, int x) { } void ListPushBack2(PListNode& phead, int x) { } int main() { ListNode* plist = NULL; ListPushBack(plist, 1); PListNode plist2 = NULL; ListPushBack2(plist2, 2); return 0; }
我们之前都是使用二级指针来进行的。但是现在,我们可以使用引用了。phead是plist的别名,和plist是一块空间。所以可以有效的避免的了plist本身为空的特殊处理。
还有一种是typedef的时候直接将结构体指针重命名,然后继续引用。上面两种尾插都是等价的
3.引用特性
1. 引用在 定义时必须初始化2. 一个变量可以有多个引用3. 引用一旦引用一个实体,再不能引用其他实体
4.常引用
我们来看下面这段代码,我们发现会报错,这其实是因为a本身自己都不可以进行改变。就想使用b去改变a这是不可能的。这里也说明了一点:引用过程中,权限不可以放大
这里我们还需要注意的是,不要搞混了引用与赋值
我们再来看这段代码,这段代码是可以的,因为权限可以平移或者缩小
我们再来看这段代码,
其中左边会报错,右边会正确,这里的原因是因为,类型转化时候会有一个int类型临时变量,而临时变量具有常属性。
所以我们让b为a的别名的时候,由于发生了类型转换,那么就会产生一个int临时变量,这个临时变量具有常属性,所以b的权限被放大了。所以报错。而右边的加上const修饰以后变为了权限的平移
类似于上面的像下面的代码也是不可以的,因为返回的时候,他会有一个临时变量,临时变量具有常性。权限放大了。
我们可以加上一个const来解决这个问题
然后这里需要注意的是,这里是权限的平移,ret1引用的不是func1()中的i,而是i在传递时候的那个临时变量,也就是说,即便以后可以修改这个i,这个ret1也是不会发生任何变化的
类似的还有如下代码也是可以的,一个是权限的平移一个是权限的缩小
5.引用的使用场景
1>做参数
引用作为参数可以充当输出型参数的作用,也可以提高效率
这块的内容,我们在上面已经提到过了。引用可以作为函数的参数,代表的是传递变量的别名,这个别名与传递变量指向的是一块空间,我们就可以直接修改这些空间的值
这些变量既可以是int、char类型等,也可以是指针变量等,都是为了充当输出型参数的作用
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
上面的是引用做参数,充当输出型参数的作用。除此之外,引用还可以提高效率,对于大对象和深拷贝类对象有提高效率的作用,深拷贝类对象我们先不讨论
大对象的意思就是sizeof出来的结果很大。我们可以使用下面这段代码来测试,来比较对于40万个字节大小的结构体,使用直接传值调用10000次和引用调用10000次的消耗时间上的区别
#include<iostream> #include <time.h> using namespace std; struct A { int a[100000]; }; void TestFunc1(A a) { } void TestFunc2(A& a) { } void TestRefAndValue() { 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; } int main() { TestRefAndValue(); return 0; }
运行结果为
可见确实是对于大对象而言,使用引用传参效率更高
引用确实是可以作为输出型参数,也可以提高效率,但是引用能做到的事情,指针也能做到。指针也可以作为输出型参数,指针传参也只需要传4个字节。
2>引用做返回值
我们先来看一下普通的变量做返回值
int Count() { int n = 0; n++; return n; } int main() { int ret = Count(); return 0; }
在这段代码中,返回n的过程,伴随着函数栈帧的销毁,n也被销毁了,那么n是如何返回给ret的呢?其实是因为编译器会先将n的值赋给一个寄存器(或一个临时变量),然后编译器再将这个临时变量交给ret。这里的不一定是一个寄存器,因为寄存器的字节比较小,只有四个字节或八个字节,数据量太大的时候可能存不下
但是如果我们加上一个static呢?,也就是下面这段代码,那么此时还会继续创建临时变量吗?
int Count() { static int n = 0; n++; return n; } int main() { int ret = Count(); return 0; }
答案是还是会的,虽然这个n并不受到栈帧的影响,但是编译器也不想搞这些特例,所以还是会继续创建一个临时变量的。一旦使用一个特例,对于编译器后面的一些优化的实现就变得更加复杂。
其实,也就是说,编译器是否创建临时变量并不取决于这个变量出了作用域后是否还存在,而是取决于返回值。如果传值返回的,无论是局部变量还是静态区的变量,那么就都要创建一个临时变量来拷贝一下。
那么如果不想要生成这个临时变量,有没有办法呢?我们说是有的,使用引用作为返回值,就不会产生临时变量。这也是传引用返回的第一大好处,减少拷贝,提高效率。
如下代码所示
#include<iostream> using namespace std; int& Count() { int n = 0; n++; return n; } int main() { int ret = Count(); printf("%d", ret); return 0; }
这段代码返回的是n的引用,也就是n的别名。我们可以将他想象为一个变量,也就是说,在主函数中创建了ret这个变量,然后再Count函数结束后,将n这块空间给返回,这里就相当于一个赋值操作。直接让n的别名赋给了ret
这种情况我们类似于引用的这种使用
#include<iostream> using namespace std; int main() { int n = 0; int& a = n; int ret = a; cout << ret << endl; }
也就是直接将n的引用赋给ret
但是上面的程序其实存在一些问题的
上面打印的ret的值其实是不确定的,因为n是一个局部变量。
如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的值侥幸是正确的
如果Count函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值
这里其实就相当于一个野指针
如果为了防止野指针的问题,我们可以将这个n改为静态的n
#include<iostream> using namespace std; int& Count() { static int n = 0; n++; return n; } int main() { int ret = Count(); printf("%d", ret); return 0; }
这样的话,这个n就不会存在万一栈帧被清理掉的话,而导致n的值出现问题的情况
我们可以测试一下传引用返回的效率
#include<iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { TestReturnByRefOrValue(); return 0; }
在这段代码中,我们定义了一个结构体,他有10000个元素的数组,然后我们定义一个全局的结构体类型的变量。我们使用两个函数直接返回这个结构体变量。最终我们测得如下结果
可见使用传引用返回确实可以减少拷贝,提高效率
传引用返回,传的是一个别名。我们在使用普通的别名的时候,我们可以直接将别名赋给一个变量,这样这个变量就直接接收了这个值,我们在上面也是这样做的
别名还有一种使用方式是,给这个别名在取一个别名,类似于如下的代码
这里的b就类似于n返回的别名,我们接收这个别名的时候,不仅可以使用一个整型变量来接收,还可以再取一次别名,这样话,ret就直接可以操控n的这块空间
类似于上面的想法,可见我们使用引用作为返回值的时候,还可以使用引用。
#include<iostream> using namespace std; int& Count() { int n = 0; n++; return n; } int main() { int& ret = Count(); cout << ret << endl; return 0; }
我们使用vs2022环境是没有清理栈帧的。所以输出结果为1
但是如果我们使用一些其他方式去破坏了栈帧,就会出现其他的情况
如下代码是因为,我们虽然栈帧销毁了,但是vs并没有清理栈帧。所以我们可以直接打印出11,而我们继续调用Count,因为之前并没有清理栈帧,我们继续再原来的函数上重新建立的栈帧,而刚好由于函数是同一个函数,所以建立的栈帧也是一样的,刚好就是再原来的ret上改成了21
如果我们中间使用其他函数去破坏栈帧,覆盖原来的值呢?那么值就变的更加奇怪了,将变成随机值
我们也可以发现,这种行为都是很危险的。
但是如果上面的都是用静态的,那么就不危险了。因为不受到栈帧清理的影响了
总结:
1.基本任何场景都可以用引用传参
2.谨慎用引用做返回值,出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回
对于引用作为返回值,其实我们还有一种用法,类似于如下的代码所示,我们使用引用的时候,不仅可以将引用作为右值,然后左值可以直接接收或者继续引用。也可以使用将引用作为左值,这样代表的就是修改这块空间的值
得益于这样的思路,当我们曾经使用顺序表的修改时候,我们需要写两个函数,一个用于得到某个下标的值,一个用于修改顺序表的值。才能解决这个问题。
而现在我们只需要一个函数即可
#include<iostream> #include<assert.h> using namespace std; struct SeqList { int a[100]; int size; }; int& SLAt(struct SeqList* ps, int pos) { assert(ps); return ps->a[pos]; } int main() { struct SeqList s; SLAt(&s, 1) = 1; cout << SLAt(&s, 1) << endl; SLAt(&s, 1) += 5; cout << SLAt(&s, 1) << endl; return 0; }
这个函数综合运用了引用作为返回值,既可以作为左值,又可以作为右值的特性。
这也就是引用作为返回值的第二大好处,可以读写返回值
6.引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。而指针开空间,存储一个地址
从 底层汇编指令来看引用实际是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点 :1. 引用概念上定义一个变量的别名,指针存储一个变量地址。2. 引用 在定义时 必须初始化 ,指针没有要求3. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体4. 没有 NULL 引用 ,但有 NULL 指针5. 在 sizeof 中含义不同 : 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4 个字节 )6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小7. 有多级指针,但是没有多级引用8. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理9. 引用比指针使用起来相对更安全
七、内联函数
1.概念
以 inline 修饰 的函数叫做内联函数, 编译时 C++ 编译器会在 调用内联函数的地方展开 ,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
2.使用
当我们在使用函数的时候,会开辟栈帧,产生消耗。并且如果这个函数需要大量重复的使用,并且比较短的时候,我们可以考虑使用内联函数
#include<iostream> using namespace std; inline int Add(int x, int y) { return x + y; } int main() { for (int i = 0; i < 100000; i++) { cout << Add(i, i + 1) << endl; } return 0; }
如果是在C语言中,那么我们只能使用宏,但是宏需要注意的事项太多了
宏的优点是:不需要建立栈帧、提高调用效率
缺点是:复杂,容易出错,可读性差,不能调试
而内联函数他不复杂,不容易出错,可读性不错,可以调试
但是内联函数只适合短小的频繁调用的函数,如果函数太长,会造成代码膨胀
而且inline对于编译器仅仅只是一个建议,最终是否会称为inline,编译器自己决定
像类似的函数加上了inline也会被否决掉
1.比较长的函数
2.递归函数
还需要注意的是:debug环境下,inline不起作用,否则不方便调试了
但是在release下又不方便看汇编了
所以我们可以这样做
然后我们就可以进入调试反汇编查看了,下面就是内联函数的调用,只要我们没有观察到call Add就代表着是内联了
当然,我们也可以使得我们的函数变得很大,以至于他不是内联了
3.内联函数的注意事项
需要注意的是:
inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。使用inline,编译器会以为内联函数使用的地方都会展开,是不会生成地址的,所以没有生成符号表。就找不到定义。
所以内联函数的定义和声明一定要在一起
八、auto(c++11)
auto他可以自动计算类型
如下代码所示
int main() { int a = 10; auto b = a; auto c = 1 + 1.1; auto* e = &a;//指定右边必须是指针,否则报错 auto& f = c }
atuo一般适合未来一些类型特别长的代码
需要注意的是:
1.当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
2.auto不能作为函数的参数
3.auto不能直接用来声明数组
九、基于范围的for循环(C++11)
如下代码所示,意思是,将arr中的每一个元素的值依次赋给e。
这种for适用于任何数组,依次取数组中的每个值赋给e,自动迭代,自动判断结束
int main() { int arr[] = { 0,1,2,3,4,5,6 }; for (auto e : arr) { cout << e << ' '; } cout << endl; return 0; }
需要注意的是:像下面这种方式是无法修改数组的值的,因为e的改变不会影响arr的改变
我们可以对e加上一个引用就可以改变了
int main() { int arr[] = { 0,1,2,3,4,5,6 }; for (auto e : arr) { cout << e << ' '; } cout << endl; for (auto& e : arr) { e *= 2; } cout << endl; for (auto e : arr) { cout << e << ' '; } cout << endl; return 0; }
需要注意的是:对于下面的代码是错误的,因为array其实是一个指针,而不是数组
十、指针空值nullptr(C++11)
我们看下面这段程序,我们发现对于NULL指针直接调用的时候,居然调用的是f(int)
对于这个现象,在早期的c++的库里是这样写的
可以看到, NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量 。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦如前面的代码,程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0 。
为了解决上面的现象,c++多了一个nullptr,也代表空指针,但是他优化了前面的缺陷
好了,本节内容就到这里了
如果对你又帮助的话,不要忘记点赞加收藏哦!!!