您现在的位置是:首页 >学无止境 >【C++】-一文让你半只脚进入C++,还不进来看看??网站首页学无止境
【C++】-一文让你半只脚进入C++,还不进来看看??
作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
C++入门
前言
hello,各位友友们,今天博客开始更新关于C++相关的知识了,C++的具体发展我就不给大家具体介绍了,有兴趣的可以自己了解一下,博主主要是分享知识点的,C++可以说是最难学的语言之一,他的语法很多,并且有难度,博主也再慢慢学习,用博客来巩固自己学到知识点,今天这篇博客就将一下关于C++的入门知识,话不多说,我们进入正文
一、C++关键字
C++是再C语言的基础上进行扩展的,我们C语言有32个关键字,那我们C++有63个关键字,多出来的31个都是再补充C语言的不足,衍生出来的关键字,我们看看是那63个关键字:
里面有一半大家都认识,因为再C语言里面看过,其他的关键字现在不认识不要紧,我们以后都会具体介绍的,这篇博客就会介绍三个关键字,接下来我们来学习两个关键字
二、namespace和using关键字
2.1namespace和using的使用
namespace的引出:
现在我们写的是C++代码,所以我们的后缀名就要变成.cpp,
上面我们说过,C++是C语言的扩展,所以我们再C++文件中也可以写C代码,我们来看一下再C++里面写的C代码:
我们可以通过::来访问全局变量,这个操作符下面会用到
1. 我们再来包一下另一个头文件:
我们看到了出现了错误,为什么会出现这样的错误,因为rand再stdlib.h里面是一个函数,再一个文件里面我们再定义相同名字的变量就会发生命名冲突。
2. 我们定义一个局部变量呢?
我们再学习C语言的时候说过域(局部域,全局域等),再同一个作用域中,我们只能有一个命名,不能再有相同的命名,防止冲突,再上面两个图,我定义一个全局变量和局部变量,这两个变量分别在不同的域中,一个在全局域,一个在局部域中,既然定义全局变量发生了命名冲突,那么包了头文件头文件相当于把头文件里面的内容在全局域中展开了,所以发生了冲突
为什么要说这个呢?因为以后再公司里面,一个项目是不同的人来写,每个人的命名风格不一样,你也不知道头文件有那些变量或者函数,如果哪天你突然包了一个头文件,导致之前定义的那个同名的变量发生冲突了,就不好了,总不能大面积的修改吧,那样太耽误时间了
这时候我们就可以使用我们namespace关键字,就叫命名空间域,相当于一堵墙,将定义的变量和函数和其他域里面的变量和函数隔绝开
我们来看具体怎么使用的:
#include<stdio.h>
#include<stdlib.h>
namespace xsm//xsm是命名空间域的名字
{
int rand = 10;
}
int main()
{
printf("%d ", rand);
return 0;
}
这样再运行就不会报命名冲突了,让我们来访问一下里面的数据吧
出现了一个奇怪的现象,为什么是一个随机值?,原因是再没有全局域和局部域时,命名空间域默认是不会去自己的域中查找所定义的内容,但有两个方式来访问到里面的数据
1.指定访问(命名空间域名::)
2.展开访问(using,给编译器查找权力,允许去命名空间域从查找)
namespace的访问方式:
1.指定访问
2.展开访问
这时候就要提到另一个关键字(using),这个关键字是让编译器可以去命名空间域中查找,相当于赋给编译器查找权力,但是using不能再namespace域体前使用,必须重新去写一下,否则语法报错
这里之所以不用上面的例子是因为上面的例子可以用来介绍其他知识点
三个域的关系:
我们再不同域中定义三个相同的变量都是可以使用的,我们就通过上图的代码改进一下,把命名空间域的访问方式改成展开的方式,并且把局部的注释:
我们发现a不明确,是不是就是类似于命名冲突,不知道访问那个,为什么会出现这个情况,原因是using展开之后是将命名空间域的内容展开到全局里面里面了,编译器对两者查找有了相同的权力,所以using尽量并且谨慎的去用,防止出现错误
我们再来看定义命名空间域那个代码:
我们看到发生了不明确,原因就是头文件的包含,里面的内容其实和全局变量再同同一个域中,然后namespace展开也相当于把里面的内容变成和全局变量再同一个域中了。
所以三个域的特点就是,只能定义唯一一个相同名字的内容,局部域永远优先,除非指定,命名空间域和全局域可以通过using来使命名空间域和全局域有相同的效果,会出现不明确的提示,所以using慎用
所以看到这里大家应该大致知道namespace的作用了吧,这个再以后项目开发的开发的时候,将自己写的变量都定义再自己的命名空间域,然后指定使用,这样再把项目合起来的时候就不会发生命名冲突的问题了,但是以后我分享关于C++知识点的时候就直接使用using了,因为我自己分享知识的时候也不会有多少代码量,并且也不可能自己再同一个域中定义相同的内容
2.2namespace和using的其他用法
namespace
1.可以定义函数,结构体
namespace xsm
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
2.嵌套定义
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
//using namespace N2;错误
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
//using namespace N2;
}
//using namespace N1;
//using namespace N2;
我们要学会嵌套怎么去指定访问,对于展开访问,可以最外层展开,也可以再上一层的里面展开,但是必须再此命名空间域后面去展开
using
我们来看一个代码:
#include<stdio.h>
#include<stdlib.h>
namespace xsm
{
int a = 10;
}
int main()
{
printf("%d
",xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
printf("%d
", xsm::a);
return 0;
}
我们上面说过,慎用using,但是也要巧用using,我们再工程里面尽量将可能冲突的变量或者其他的,使用指定的方式,如果有常用的,我们都需要指定那是不是太麻烦了,我们可以展开指定
using xsm::a;
我们看看这样是不是就是方便很多,希望大家也可以学到这个小细节
三、C++输入&输出
新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物,那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}
说明:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
- <<是流插入运算符,>>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
- 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,
这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自动识别变量的类型
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,这些又用得不是很多,我这里就不展开学习了。比如控制打印几位小数,我们可以直接使用printf就可以了,cout也可以,大家可以上网查查,用的不多,博主这里就不给大家多介绍了
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
- 在日常练习中,建议直接using namespace std即可,这样就很方便。
- using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
这也是我之前说过的using的巧用
以后我写博客就直接带< iostream>和std,大家也不会感到迷惑了
四、缺省参数
4.1缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
来看代码:
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
4.2缺省参数分类
全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
半缺省参数
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给
我们想想,从左往右给为什么不行:
void Func(int b = 10, int c = 20, int a)
因为没有缺省值的必须要接收一个参数,当函数为Func(1);只能给缺省的b,会报错,传的参数使依次给形参的,
- 缺省参数不能在函数声明和定义中同时出现
#include<iostream>
using namespace std;
void Func(int a, int b = 12, int c = 20);//声明
void Func(int a,int b = 10, int c = 20)//定义
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
因为设计者怕如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。那我们应该哪个有缺省值呢?再声明还是定义??
再C语言学习的阶段,我们知道函数如果调用的其他函数,并且他的定义再此函数的下面,哪我们是不是要再此函数前面声明一下,因为编译器使从上往下开始读代码编译的,如果不提前声明,告诉编译器我有这个函数,那读到此函数的时候,遇到其他函数就不认识,如果声明了,就可以找到定义的函数了,那缺省参数应该放在那里呢?我们是不是通过声明来找定义,那我们缺省参数是不是直言放在声明的时候就行了,到时候找到了再传给定义,如果声明和定义都有缺省参数,那么声明里面的缺省值传过来和定义里面的缺省值不一样就会发生冲突,有的人会说,我自己声明和定义怎么会不一样呢??但是以防万一,设计者还是将这种都可以写的语法默认不行, 所以我们只能将缺省值放到声明里面
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
大家现在只知道缺省值的概念和大致用法,我给大家举一个熟悉的例子:我们之前学习过动态的顺序表,我们需要初始化,我们来回顾一下代码:
struct seqlist
{
int* a;
int size;
int capacity;
};
void seqlistinit(struct seqlist* sq)
{
sq->a = (int*)malloc(sizeof(int) * 4);
if (sq->a)
{
perror("malloc");
return;
}
sq->size;
sq->capacity=4;
}
我们知道,到数据多的时候,肯定就要扩容,扩容肯定就有消耗,那如果我们提前知道有100个数据要插入,那就要扩容好几次,但是我们使用缺省参数的话就很方便,
void seqlistinit(struct seqlist* sq,int capacity=4)
{
sq->a = (int*)malloc(sizeof(int) * capacity);
if (sq->a)
{
perror("malloc");
return;
}
sq->size;
sq->capacity=capacity;
}
int mian()
{
seqlist sq;
seqlistinit(&sq, 100);//提前知道要插入多少元素的时候
seqlistinit(&sq);//不知道的时候
return 0;
}
这样就可以提高效率,再C语言的时候,我们通常是定义宏来解决,但是不能想我上面那样传参,但是可以解决知道提前要插入多少个数的场景,并且只有一个栈的时候,但我们再一个文件里面,有定义多个栈,有的知道个数,有的不知道,那么C语言的弊端是不是就体现出来了,而缺省参数刚好解决这个问题,所以说C++是C语言的扩展,并且也是弥补C语言的不足
五、函数重载
5.1函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
我们来看一下重载的例子:
这就是函数的重载,使用相同名字取完成类似的功能,这样解决了取名困难的问题,我们来看函数重载有那些具体用法和要注意的细节
5.2函数重载的细节
细节:
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
//4.参数相同,变量不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(int b, char a)
{
cout << "f(int b, char a)" << endl;
}
5.返回类型不同
int f(int a, char b)
{
cout << "f(int a,char b)" << endl;
return a;
}
void f(int b, char a)
{
cout << "f(int b, char a)" << endl;
}
前三个是可以的,后两个是不可以的
容易出错的函数重载
我们再来看看这个代码:
void f(){;}
void f(int a=1){;}
大家觉得这是不是函数重载??答案:是重载,我们抓住常在的定义就行了,符合定义是重载,但是再不传参调用就会报错,因为不明确
void f(int a){;}
void f(int a=1){;}
这种代码类似,传参的时候就会报错:
函数重载再后面会用到的更多,而且会感觉他非常的好用,其实我们现在也使用了一些函数重载,再我们输入输出的时候,<<和>>对这个两个运算符使用了函数的重载,cout和cin是类的对象,调用这个两个函数,来实现可以输入输出不同类型的数据,这样就不需要像C语言一样写打印的格式,大家可以参考这篇文章看看coun和cin的函数重载
接下来我就深入带大家理解为什么C++可以使用函数的重载,而C语言不行,这时候就要用到之前讲过的程序的预处理相关的知识程序的预处理
5.3函数重载的底层原理
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
我们来回顾一下程序再这几个阶段会处理那些事情:
预处理:再这个阶段,我们我们会进行头文件的展开,删除注释,还有宏定义的替换
编译:再这个阶段,我们会检查语法错误,和检查函数的地址是否正确,像函数声明的时候就会被检查,编译性错误就会再这个阶段产生,就不会进行下一个阶段
汇编:再这个阶段,我么们所检查的地址会形成符号表,然后通过符号表的地址去找函数的定义,但此时还没有去找,只是形成了,再链接的部分才会查找,并且把汇编语言变成二进制语言,我们看不懂,形成符号表我们也看不懂
链接:再这个阶段会通过符号表里面的地址会和定义的地址进行链接,如果没有对应的地址,那么程序就会报链接性错误,我们调用函数就是这个原理,去找对应的函数定义
我们的示例代码:
#include<stdio.h>
int add(int a,int b)
{
return a+b;
}
int main()
{
printf("%d",add(1,2));
return 0;
}
怎么再Linux编写代码,参考这篇博客Linux环境搭建
对于C语言
我们再调用函数的时候,怎么去通过函数名去查找呢??
我们通过Linux来看,因为简单写而且直观些
1.先生成可执行文件
gcc -o tetsc test.c//并且命名为testc
2.再查找反汇编指令
objdump -S tetsc
C语言直接通过函数名来查找,那么既然这样,如果定义多个相同函数名的函数就会发生冲突
对于C++
1.
g++ -o testc test.cpp
objdump -S tetsc
我们看到再C++的函数名就有所差距,我们来看看分别代表什么意思吧
【_Z+函数长度+函数名+类型首字母】
这时候大家应该知道,我们为什么再函数重载的时候,要规定参数个数或类型或类型顺序不同了吧,并且为什么仅返回值不同的不构成函数重载的原因也再这,因为不在命名规则里面里面,有的人说可以加上这个规则,也可以写成 返回类型->函数名,但是创建C++的人认为这样不可以,那就不可以
讲底层的原理希望大家可以更号的理解函数的重载,也为了后面更好的学习,这一部分讲解有错误或者不好的地方,还希望读者指正出来
六、引用
引用可以说是C++一个比较重要的知识点,他的出现解决了C语言好多看起来麻烦的代码,让程序变得通俗易懂,但是再学习引用的时候有许多细节,而且最好要理解一下变量在内存是怎么开辟空间的,和内存的分布情况以及每块对应的特点,还有函数栈帧相关的知识,那我们接下来
1.看看引用到底是什么?
2.理解引用时怎么使用的?
3.引用适合什么场景以及相应的例子
6.1引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
使用这个三个称呼都可以让上面这个人答应,所以引用取别名相当于取外号,和本身变量要完成的事效果一样。
具体语法:类型& 引用变量名(对象名) = 引用实体;
1.引用实例
我们看到两个地址是一样的,所以b就是a,a就是b,谁的改变就会影响另一个
2.多重引用
多重引用是总结取的一个名字,和引用一样,但也有自己的意思,可以给别名取别名,就好比一个人的外号不止一个一样的道理
我们看到所以的引用别名都是同一块地址,都是代表变量a,a有三个外号了,使用谁都可以操作a
3.引用的使用
也就是当成变量来使用
在这里每个引用变量都可以来改变a的值
4.引用的唯一性
我们定义的引用再同一个域中只能给一个变量取别名,就好比再同一个班级,不可能给两个不同名字的人取相同的外号,那么叫外号的就会发生冲突
5.引用的目前的小细节
目前的细节就是我们看了上面三点,中间有那些细节呢??
(1)图解
(2)大家看到引用是&操作符,和C语言的取地址操作符一样的,大家可能一开始看引用有点不习惯,告诉一个方法,现把他想成指针的*号,等会我介绍多了大家熟悉了,就可以不用这么想了
&操作符只有在类型后面才能使引用,没有类型在前面,其他地方就是取地址操作,希望大家要明白
(3)引用的定义一定要初始化,就好比有那么一个人,你才能给这个人取外号,没有这个人,没有办法取,引用也是一样的,都没有这个变量怎么给变量取别名呢??所以我们一定要初始化
特性:
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
- 只要是普通变量(没有修饰,没有特殊含义)都可以用引用
大家现掌握着几个细节,一会再慢慢补充,因为大家看到这里对引用也只有一个初步认识,了解了他的概念,并且不知道有啥好处,也就是换了一下操作符,和指针差距不大啊,那么接下来我就要介绍引用的应用场景,并且他的使用给我们变成带来那些好处
6.2引用的使用场景
引用使用的场景也非常的多,而且注意的细节也多,不注意引用就会变的特别危险,那我们具体来介绍一下
1.做参数
大家还记不记得我们之前将过怎么通过函数来交换两个数,之前是通过传地址,原因就是形参是实参的一份临时拷贝,知识把实参的值给了形参,两块并不是同一块地址,形参的改变不会影响实参,我们传地址的目的就是把实参的地址传过去,让函数再我传给你的地址上进行修改这样就可以达到目的
那我们用引用来接收,不就是来接受实参的空间吗??两个地方是一样的,可以理解把上面的引用实例等号左右分开,一个放到形参里面,一个放到实参里面,来看具体代码:
#include<iostream>
using namespace std;
void swap(int& a1, int& a2)
{
int tmp = a1;
a1 = a2;
a2 = tmp;
}
int main()
{
int a = 10; int b = 20;
cout << a << " ";
cout << b << endl;
swap(a, b);
cout << a << " ";
cout << b << endl;
return 0;
}
大家是不可以清楚的看到引用的有点,使代码不会那么的繁琐,再指针还要解引用
相信大家有的刚接触到数据结构的同学,也有的学完了数据结构的同学,相信大家再书中一定看过这样的代码:
typedef struct Slist//定义一个顺序表
{
int* a;
int size;
int capacity;
}Slist,*plist;
void SlistInit(plist* a);//初始化函数声明
void SlistInit(Slist& a);//初始化函数声明
相信大家看书上出现这个代码肯定是迷糊的,我来给大家分解一下
这样就不用传地址,再解引用来初始化数据了,还可以给指针取别名,只要是开放变量都可以取别名,让我们更好的理解,但书上的这种做法有点适得其反了,不了解引用的更看不懂了,希望大家现在可以理解上面的代码
2.做返回值
我们先来了解一下函数的返回机制,再函数的返回的时候,是直接将值返回过去的吗??答案是不对的,我们函数再返回的时候,再出函数前,会将数据保存再一个临时变量里面,这个是有编译器去实现的,然后出了函数,再把值赋给接受方,我们来看一个例子:
int count()
{
int n = 10;
n++;
return n;
}
int main()
{
int ret = count();
return 0;
}
这个图可以给大家一个大致的理解,再函数开辟的时候徽再栈区形成栈帧,里面的变量是临时的,也是再栈区开辟的,是静态的,就在静态区开辟的,如果变量是再栈区开辟的,会随着函数的销毁,这个理解再后面很关键
重点:函数销毁的具体理解是:我们再销毁函数栈帧的时候,其根本的本质是将那一块空间的使用权还给操作系统了,而不是这一块空间没有了,即使使用全不是我们的了,但是操作系统清理这块空间或者没有被其他函数所占用空间,那么我们的数据是不是还在那一块空间上,知识我们不能去使用他的,但是看看他可以的,这也是我们之前说到数组,为什么越界还能看到打印的数据,即使是随机值,但是我们要修改里面的内容就会报错。这里理解了,我们开始进入下面的正式讲解
为什么使用引用做返回值呢??他又是怎么使用的,好处和不足的地方再哪里??我们来探讨这样的问题
(1)为什么使用引用做返回值呢??
上面我们介绍过,函数返回的时候需要创建临时变量,这样就会有消耗,使用引用返回的就是那个变量的空间,中间没有消耗,所以我们选择了使用引用作为返回值。而且引用大大减少我们代码的复杂度,我来写一个案例,让大家提前感受一下,可能目前还不能理解,但是学完这一届你就会明白:
typedef struct Slist//定义一个顺序表
{
int* a;
int size;
int capacity;
}Slist,*plist;
void SlistInit(Slist& phead,int capacity=4)//初始化顺序表
{
phead.a = (int*)malloc(sizeof(int) * capacity);
phead.size = 0;
phead.capacity = capacity;
}
void SlistPush(Slist& phead, int pos, int x)//再任意位置插入
{
phead.a[pos] = x;
phead.size++;
}
int Slistget(Slist& phead, int pos)
{
return phead.a[pos];
}
void SlistPrint(Slist& phead)
{
for (int i = 0; i < phead.size; i++)
{
printf("%d ", phead.a[i]);
}
printf("
");
}
int& get(Slist& phead, int pos)//用引用作为返回值
{
return phead.a[pos];
}
int main()
{
Slist s;
SlistInit(s);
SlistPush(s, 0, 1);
SlistPush(s, 1, 2);
SlistPush(s, 2, 3);//再不同的位置插入数据
printf("%d
",Slistget(s, 1));//获取任意位置的地址
SlistPrint(s);
get(s, 0) = 4;
get(s, 1) = 5;
get(s, 2) = 6;
printf("%d
", get(s, 1));
for (int i = 0; i < 3; i++)
{
printf("%d ", get(s, i));
}
return 0;
}
再是顺序表的一些操作,里面的细节我没有完善,我想表达的,插入数据,获取数据,打印数据,原来需要三个函数完成,现在只需要写一个就写了,完全当成数组来用,这就是用引用做参数的好处,这个大家现不理解不要紧,我开始从简单的给大家介绍
(2)做返回值的使用
我们来看一个简单的例子:
int& Count()
{
static int n = 10;
n++;
// ...
return n;
}
我们来看图解:
大家看到返回的就是n在静态区的空间别名,在用另一个别名接收一下:
int& ret=count();
大家应该注意到一个问题,我在创建变量的时候,用的是静态的变量,原因是,静态变量在函数销毁的时候不会被释放,空间还可以继续去使用,如果使用局部变量,返回的空间的别名虽然不会报错的,但是那块空间不是的使用不是我们了,万一有人用这个别名修改了数据就会出现错误,像我们传数组,或者malloc出来的,因为这一块的空间都是在堆区,不在栈区,不会随着函数的销毁而销毁,这时候用引用返回最好。
(3)不足以及危险性
我们来看一下不加static会出现什么结果:
不被清理的情况
int& count(int x)//传一个参数更好的演示
{
int n=x;
return n;
}
int main()
{
int& ret = count(10);
cout << ret << endl;//大家认为输出10
count(20);
cout << ret << endl;//大家可能会认为这个也输出10
return 0;
}
运行结果:
为什么第二个是20??原因就是第一次调用count函数,系统给他开辟了栈帧,在里面有一部分给变量n分配的,但结束调用,空间使用权还给操作系统,但数据没有被清理就还在,所以第一还是可以访问ret的,只要中间随便出现一个函数,那块空间被其他函数给占用,访问的可能就是随机值那么我们在调用一次count函数,里面又有一块空间是n的,数据为20,和上面相同的道理数据还是在的,没有被清理或者占用,所以访问的值为20,来看一下图解:
被其他函数占用的情况:
之前那个空间被printf函数占用使用权就是打印函数的,里面的数据在调用打印函数的时候被修改了,所以是随机值
大家看到这里应该明白了吧,所以局部变量用引用很危险,取决于你返回的变量出函数后是否被销毁,也取决于栈帧是否被其他函数开辟的空间所调用,遇到上面的情况就老老实实用值返回吧
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
总结:
将总结理解了,上面讲的也几乎理解了
说了这么多,引用做返回值的优点和注意事项,基本任何时候都能用引用传参,谨慎用引用做返回值,出来作用域,对象不在了,就不行
6.3 传值、传引用效率比较
我们来看看传值和传引用的效率对比,传参和返回值的时候都是一样的道理,不是直接传给参数,都是借助临时变量来传递的,所以创的对象越大,传值,创建临时变量消耗就越大,我们定义一个大变量来看看:
struct A{ int a[10000]; };
我们来看看测性能函数:
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;
}
我们看到对象越大效果越明显
值和引用的作为返回值类型的性能比较,和上面测试方法差不多,这里我就不做过多的介绍,大家下来自己测试
总结:用引用减少了中间创建临时变量的步骤,提高效率,但是容易出现访问不是属于自己的空间有危险,
说了关于引用,想必大家他和指针的区别,无非把之前的*号改成&,返回的时候或者传参的时候在加一个&不久行了,我们就来看看两者的区别
6.4引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
我们来看案例:
int main()
{
int a = 0;
int& ra = a;
ra = 20;
int b = 10;
int* p = &b;
*p = 20;
return 0;
}
我们看反汇编,在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
那么这样,他两者到底有什么不同点:
-
引用概念上定义一个变量的别名,指针存储一个变量地址。
-
引用在定义时必须初始化,指针没有要求
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
-
没有NULL引用,但有NULL指针
-
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
-
有多级指针,但是没有多级引用
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
-
引用比指针使用起来相对更安全
两者相辅相成,引用解决了许多指针出现的不足,和多级指针带来的代码可读性差,引用让代码看的清晰明了,所以像很好的用号引用,还是要花时间去琢磨,和指针对比着去比较,这样才能更好的掌握指针,后面会大量用的引用,大家一定要弄明白,不然后期很麻烦
6.5 常引用
常引用涉及到权限的放大和缩小,还有平移相关的知识引用的权限可以缩小,但是不能放大。
我们来看一下代码:
void TestConstRef()
{
const int a = 10;//a的权限是不可变修改
int& ra = a; //这是可以随便修改的引用,权限放大了不可以
const int& ra = a;//这将引用权限又缩小成不可修改的,也可以说是权限的平移,所以可以
// int& b = 10; 10为常量,不可被修改,现在引用b的权限是可以任意修改的,不可以
const int& b = 10;//缩小成不可随便修改的,也可以说是权限的平移,所以可以
double d = 12.34;
int& rd = d; // 左右两边权限一样的,都是可以任意修改,但是类型不行,所以不可以
const int& rd = d;//权限的缩小,两边类型可以不用管,所以可以
}
我们来看一个案例:
int main()
{
int x = 0;
int& y = x;//权限的平移
const int& c = x;//权限的缩小
c++;//1
x++;//2
return 0;
}
大家看第一处和第二处有没有错误,答案是第一处有错误,第二处可以,原因是权限缩小的是别名的权限,原来变量的权限还是没有缩小了,知识不能通过别名去对原来变量进行操作了
再来看一个实例:
int add(int a, int b)
{
int z = a + b;
return z;
}
int& add(double a, int b)
{
int z = a + b;
return z;
}
int main()
{
int ret = add(1, 2);//直接用变量去接收,可以
int& ret = add(1, 2);//这个不可以,因为返回的值,是具体的数,不可被修改所以这样写相当于权限的放大
const int& ret = add(1, 2);//权限的平移。可以
int& ret = add(1.1, 2);//用引用返回,用引用接收,可以
const int& ret = add(1.1, 2);//缩小了权限,可以
int ret = add(1.1, 2);//用变量接收,可以
return 0;
}
大家把这些弄懂了常引用几乎不用担心了,我们开始讲另一个知识点
七、内联函数
我们在之前说过,调用函数的时候需要开辟栈帧,既然开辟栈帧就需要消耗时间,调用一次没啥变化,调用好多次,差距就体现出来了
int add(int a, int b)
{
return a + b;
}
void TestReturnByRefOrValue()
{
//调用100000次
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
add(1,2);
size_t end1 = clock();
//调用1000000次
size_t begin2 = clock();
for (size_t i = 0; i < 1000000; ++i)
add(1,2);
size_t end2 = clock();
// 计算函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
运行结果:
大家应该看到了差异了,那我们在C语言是怎么解决这个事情的呢??
我们采取宏定义来解决,因为宏定义不需要开辟栈帧,只需要替换就好了。
#define add(x,y) ((x)+(y))
使用宏要注意该加括号的加括号,因为是直接替换,所以不加括号会带来不必要的麻烦
内联
使用宏虽然可以解决这个问题但是容易出错,代码常一两行用宏就实现不了了,所以这个时候我们就用另一种方法来解决这个问题,使用内联,不容易出错,而且也不需要开辟栈帧,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
inline int add(int a, int b)
{
return a + b;
}
我们来看看没有内联,看看汇编会出现什么:
出现call就代表创建了函数栈帧,加了内联在debug版本下看还是有call,我们必须在release版本下去调试,因为release版本不支持调试,所以我们必须修改一些配置:
没有调用的call指令,直接展开了函数的指令没有开辟栈帧的消耗,有的人会说既然内联这么号,那所有的函数都使用内联不就好了,实际上是不可以的,他有要适用条件
内联的适用条件
在没有内联的时候,假如一个程序有10000个地方调用这个函数,而每个函数的汇编指令有50条,那我们有要执行此程序至少需要10000+50条指令即可,没调用一次就会call一次去调用,但是你要是使用inline,内联函数,就至少需要执行10000*50这个差距还是非常明显的,所以在内联只适用于函数代码少的,这样形成的汇编指令也会少,假如此时函数指令有2条,那么调用10000次,不用内联就是10000+2,用内联是20000,还是一个量级的,第二个是此函数频繁的调用,你就调用一两次用不用内联都一样,开辟栈帧几乎消耗不了多少时间
总结:内联函数只适用于函数代码少频繁的调用的时候。
内联的注意用法
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 中被引用
内联的特性
-
上面说的如果函数内部代码多,用内联,展开后的汇编指令会非常多,汇编最后会转换成可执行的程序文件就会非常的大,这就好比你下载软件的大小很大,所以我们一般都不希望自己适用的软件大小太大。这也是我们内联的特性之一
-
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性
希望大家看到这里对内联的概念和用法应该大致了解了,我们接下来介绍新的知识点
八. auto关键字(C++11)
8.1 类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
std::map<std::string, std::string>::iterator//这个后面会介绍,你现在只要知道这是一个类型就行了
但是该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名。
我们适用typedef定义变量虽然没问题,但是如果要接收的函数的返回值,而且不知道返回值是什么的情况下,这种typedef就不太好实现
这时候就需要一个可以自动识别类型的关键字,就使用auto关键字
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
8.2 auto的使用细则
- auto与指针和引用结合起来使用用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
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的初始化表达式类型不同
}
8.3 auto不能推导的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a){}
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
九. 基于范围的for循环(C++11)
9.1 范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
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来跳出整个循环。
9.2 范围for的使用条件
- for循环迭代的范围必须是确定的对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
- 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)
十. 指针空值nullptr(C++11)
10.1 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。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr**
十一、总结
博主花了很长时间把C++入门的知识点给大家说清楚了,也希望读者可以更好的接触C++,里面的内容非常丰富,学完这里面的内容九可以接触面向对象了,到时候在和大家讲解相关的知识,今天的知识九分享到这里了,我们下期再见