前言:
本篇将结束c++的一些基础的语法,方便在以后的博客中出现,后续的一些语法将在涉及到其它的内容需要用到的时候具体展开介绍;其次,我们需要知道c++是建立在c的基础上的,所以c的大部分语法都能用在c++上。
1.域作用限定符
首先,认识一下域作用限定符:
#include <iostream>
int main()
{
std::cout<<"hello world"<<std::endl;
return 0;
}
代码中域作用限定符就是::,当我们遇到在用一个项目组中使用了相同的命名的时候,除了改该怎么办?我们可以指定对应的域中去找相应的重复命名的内容,例如去某个结构体或者类中去找(类在后面会介绍);其次记住一点,如果域作用限定符的左侧为空,就是去全局里面去找。
2.关键字namespace
#include <iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
首先可以明显的看到,函数里面的内容少了点什么,这就是因为我们加上了一句using namespace std;,为什么呢?
这里就开始介绍初学c++为什么要这样写程序了,因为大部分人学会使用了c++却连开始为什么要这样写都说不上一二,就有些尴尬了,首先介绍开始的头文件:
我们查阅文档可知(来源:https://legacy.cplusplus.com/reference/)iostream中包含了cin和cout等(其实它们都是iostream的对象),所以我们要想使用cout和cin等就要包含这个头文件。
那cout和cin等又是什么?我们目前记住它们是用来显示屏幕的信息和提取屏幕的信息的,分别需要搭配流插入<<和流提取>>,所以它们就都相当于c中的printf和scanf,但是不同的是它们不是库函数,而是某个类的对象,后面再提,以及与printf,scanf的区别也在后面再提。
然后endl只需要知道它是换行的意思即可。
然后就是我们的命名空间这一行,这一行是什么意思呢?首先我们需要知道
- 用法:namespace name
{}
- 命名空间---命名空间域,只影响使用,不影响生命周期,在多个文件使用相同的命名空间会被认为是同一个
然后我们需要知道std是c++标准库的名字,也就是说如果我们想用封装在c++标准库中的内容就需要展开这个命名空间,也就是展开标准库所在的域,这样才能使用库中的内容,如果不这样使用,cout等就会不被编译器所认识。
但是需要注意,你会在一些人的代码中发现是这样写的:
std::cout<<"hello world"<<std::endl
为什么呢?首先我们知道了命名空间的存在其实就是为了封装起你写的内容,避免其他的重复命名出现导致出问题,而如果要让标准库中的域展开,那就破坏了这道防线,因为其实封装的目的就是防止你的命名与标准库中的冲突,再使用与标准库中同名的就不行了,所以就有了这样的写法(平时练习可以直接展开,但比较大的项目中不建议这样用)。
如果我们将上方的写法称为全部展开,那下面的写法就为部分展开:
using std::cout;
using std::endl;
也就是让比较常用的展开,避免一个一个写。
3.流插入和流提取
刚刚我们已经提到了它们,例如cout<<endl就是让换行符流到控制台上,再看一个流提取的例子:
int n=0;
double* a=(double*)malloc(sizeof(double));
if(a==NULL)
{
perror("malloc fail");
exit(-1);
}
for(int i=0;i<n;++i)
{
cin>>a[i];
}
for(int i=0;i<n;++i)
{
cout<<a[i]<<endl;
}
这是一个输入数组元素,然后打印出来的例子,从中我们可以看到cout和cin的用法,但是我们也可以发现我们在使用cout和cin的时候好像不用管a是一个double类型的数组唉?
这就是流的特性:
- 流会自动识别类型,不需要%d%f等输入输出
至于为什么,我们将在流的内容与运算符重载中在提。
4.缺省(默认)参数与缺省值
我们知道,在c语言中,函数的参数是不能赋值的,也就是说你传来的什么就是什么,而c++有创造了一个新的玩法:
void func(int a=0)
{
cout<<a<<endl;
}
int main()
{
func(1);//传参就是打印传的值,也就是1
func();//不传就是打印默认值0
return 0;
}
至于有什么用,我们在以后再说(减少重载函数的数量,简化函数的调用过程)。
a.全缺省参数
全缺省就是所以参数都缺省:
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a=" << a << endl;
cout << "a=" << b << endl;
cout << "a=" << c << endl;
cout << endl;
}
int main()
{
Func(1, 2, 3);
return 0;
}
需要注意的是,只能从右往左缺省,且要连续,为什么呢?
因为如果你要写成:
void Func(int a = 10, int b = 20, int c )
那你传两个参数的时候,c对应的位置必须要有一个传过来的参数(因为没缺省嘛),那第一个实参是对应的a还是b,不仅编译器会报错,意义也不明;参数从右往左缺省就不会出现这样的问题。
然后就是使用缺省值要从右往左(因为参数是从右往左缺省的)连续使用,像这样不连续的就不行:
Func(,1,2);
b.半缺省参数
也就是参数可以不完全缺省,规则也和全缺省一样(从右往左连续缺省),不同的是不能不传参数,如果传一个参数就是对应最左边的参数
c.注意
- 不能在声明和定义中同时给缺省参数,要在声明中给(如果声明和定义的缺省值不同,编译器无法确定,放在声明中也减少了其他源文件重复定义;还有些人说是为了保护代码的安全,因为别人copy了源码却不知道缺省值)
- 缺省值必须是常量或者是全局变量,c语言不支持缺省参数
5.函数重载
在c中,我们知道使用函数名相同的函数是行不通的,所以本贾尼博士又发明了一个新的语法,允许函数名可以相同,但是参数的个数或者返回类型或者类型的顺序要不同的同名函数存在。
void Add(int a,int b);
void Add(int a,int b,int c);
void Add(int a,int b);
void Add(double a,double b);
void Add(int a,int b);
void Add(int b,int a);
那具体是怎么实现相同的函数名能找到对应的函数呢?
c++对函数名进行了修饰(此为linux的修饰规则,不同平台的修饰规则不同),例如void Add(int a,int b),会修饰成_Z3Addii,void Add(double a,double b)会修饰成_Z3Adddd,其中_Z是固定的,3是函数名长度,再加上函数名,再加上形参的类型缩写,通过不同的函数名修饰加上编译时产生的地址,就能区别出调用哪个了(浮点数默认为double)
另外还需要注意:
- 返回值不同的,其他都相同,不构成重载,因为编译器识别不出来,不是因为修饰名没有返回值而识别不出,而是调用函数的时候没写返回值(call这个函数的地址的时候没有返回值),区分不出来
- 编译时就已经确定了调用那个函数 ,编译速度可能变慢,但是运行速度不会变慢
6.指针和引用
a.语法
可以看到k是i的引用,并且地址是一样的,所以我们又说k是i的别名。
同时我们还可以看到,直接赋值地址是不一样的,因为是又开辟了新的栈帧。
引用在后面的应用有很大的作用。
注意:
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
b.应用
- 应用1(输出型参数)---实参影响形参的情况,例如改变实参的值就要传实参的地址,从而形参要用指针
void Swap(int& x,int& y)//形参是实参的别名,地址一样,不用取地址
{
int tmp=x;
x=y;
y=tmp
}
- 应用2(做返回值)
首先,先引入传值返回:
我们可以看到:
返回的n先存到(拷贝给)一个临时变量(可能是寄存器也可能是上一层栈帧),然后count栈帧销毁,这个临时变量再给给ret,如果n是个静态的变量放在静态区,也是先存到临时变量,也不会直接返回n,此为传值返回(用于变量出了作用域就不在的情况)
如果使用传引用返回:
返回n的别名,减少了拷贝,因为n已经是静态的了,但返回时还要拷贝到一个临时变量,返回n的别名就减少了拷贝(可以理解产生了一个临时变量,但这个临时变量是n的别名,也就是n,就减少了拷贝 ),只要出了count作用域这个变量还在,就可以用传引用返回
所以使用传引用返回,我们可以得到以下结论:
- 减少拷贝
- 调用者可以直接修改返回的对象
我们再来看一个错误的例子:
int& Add(int a,int b)
{
int c=a+b;
return c;
}
int main()
{
int ret=Add(1,2);//正确的
int& ret=Add(1,2);//错误的
}
返回c的别名,ret又是c这块空间的别名,但是c这块空间销毁了,访问ret也是找到销毁的空间,如果c的空间被覆盖就是随机值,没有就还是那块空间
总结:
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回(性能上有一定的提升),如果已经还给系统了,则必须使用传值返回,以此理解全局变量二者都可以(因为全局变量也是存在静态区的,所以也是先拷贝给临时变量)。
c.常引用
const int c=2;
const int& d=c;
const int* p1=NULL;
const int* p2=p1;
const int m=1;
int n=m;
int count{int n=1;return n;}
const int& ret=count();
int i=0;
const double& rd=i;
从前两个例子我们可以知道d是c的别名,c是不可改的,那d也应该是不可改的;同理指针也是(*p不能修改,但是p可以修改,需要根据const的位置,在c专栏中有介绍到这一点),
赋值或初始化,权限可以缩小(只有别名是const修饰),但是不能放大。
而第三个例子就仅仅赋值,所以n的改变不会影响到m。
第四个例子是count的返回值是n先拷贝给临时变量,再给ret,返回的这个临时变量(在此过程中)也不可改变,它的别名也要用const修饰。
第五个例子是隐式类型转换,类型转换会产生临时变量,i先给给double类型的临时变量,临时变量再给给rd,所以rd是这个临时变量的别名,而这个临时变量(因为在这个过程里面这个临时变量肯定不变嘛)具有常性,所以要加const。
d.指针和引用的区别
首先我们需要知道:
- 从语法的角度,引用不开空间,指针需要开空间;而从底层的角度,从汇编来看二者过程一模一样,引用也是需要开空间的
其次不同点:
这里的安全是从引用没有空引用个定义必须初始化出发的,因为有时指针的一些错误不会直接报错。
7.内联函数inline
a.复习宏
c++推荐使用enum和const代替宏常量,inline代替宏函数。
我们来复习一下宏的优缺点:
优点:
- 类型可以不固定
- 不用建立栈帧
缺点:
不能调试
没有类型安全的检查
有些场景非常复杂(例如add宏函数)
#define ADD(x,y)((x)+(y))
1.为什么x+y要加括号,因为宏只是替换,例如ADD(1,2)*3;不加括号就达不到效果了
2.为什么里面的x和y分别要加括号?因为例如ADD(a|b,a&b);宏会替换成a|b+a&b,而加号
优先级更高,也达不到想要的效果
3.加分号也不行ADD(1,2)*3;宏会替换成((1)+(2));*3;会报错
b.使用
inline int Add(int x,int y)
{
int z=x+y;
return z;
}
用汇编查看可知在调用内联函数时就展开了函数的逻辑,不用建立栈帧:
如果存在call Add就是没有展开函数,也就不是内联;红色箭头指向的就是没有call 这个函数的地址,直接展开函数体,使用了内联。
c.内联函数的性质
关于第三点:
假设在头文件中声明一个内联函数,再在函数实现文件中定义一个内联函数,此时在主函数中使用内联函数就会链接报错,因为虽然主函数引用了头文件,但是内联函数是直接展开的,没有地址,头文件只定义了内联函数,但找不到内联函数的地址(也就是出现在了声明中,但是展开时找不到函数体),所以建议定义和声明都在头文件里。
注意:
如果成员函数在类中定义,编译器可能会将它当做内联函数处理。
所以在stl源码里面有些短小的函数直接就放在类里面定义了,就是当做了内联函数
有些大一点的函数就类中声明,类外定义,源码就这样搞的,所以声明定义分离还是有一定的道理的。
8.关键字auto
a.语法
int a=0;
auto b=a;
auto c=&a;
cout<<typeid(b).name<<endl;//int
cout<<typeid(c).name<<endl;//int*
实际价值是为了简化类型很长的代码。
b.缺点
- 不能做形参
- 不能声明数组
- 定义变量必须初始化
c.特性
- auto后加*或者&就是限定类型为指针或者是引用
auto a=1,b=2; auto c=1,b=2.0;//报错
同一行定义多个变量时,这些变量必须是相同的类型,否则编译器会报错,因为编译器是根据第一个类型进行推导,再用推导出来的类型定义其他的。
d.typedef的缺点(附加)
typedef char* pstring;
int main()
{
const pstring p1;//报错,需要初始化
const pstring* p2;//正确
}
这里所说的缺点就是如果是const char* p1,就不会报错,因为const修饰的是p1指向的空间,可以不对p1赋初始值,但是使用typedef就不行了,这里会被误解为const char p1,const修饰的是p1,要对p1赋初始值。
9.范围for(c++11)
int array[]={0,1,2,3};
for(auto e:array)//自动依次数组中的数据赋值给e对象,自动判断结束,所以e的改
{
cout<<e<<" "; //变不会改变数组的值,想改数组值用auto& e
}
cout<<endl;
c++11提供的语法糖,底层其实是迭代器,知道怎么使用就行(分号前是范围内用于迭代的变量,右边是被迭代的范围)
这里auto加上了引用是因为数据量大且涉及深拷贝(以后还会介绍)。
10.关键字nullptr
c++里NULL(NULL是一个宏)被定义为0,识别出来是int,实际c++不定义应该是((void*)0)