一、关键字
C语言中只有32个关键字,C++有63个关键字,将近翻了一倍。
二、命名空间
在编写代码的时候,会遇到定义的变量名和库中的函数名重复,出现命名冲突的情况。在C++中有变量名、函数名还有类名,这些都会存在全局域中,在大项目中很难避免命名重复的问题。为了解决这种问题,通过namespace关键字去命名一块空间,用于存放变量名、函数名还有类名。而这种空间通称为命名空间,命名空间中的定义的这些变量、函数或者类其实还在全局域(静态区)中。
1.命名空间的使用
首先需要定义一块命名空间
#include<stdio.h>
namespace new1
{
int a = 0;
int b = 10;
}
int main()
{
//printf("%d\n", a);//错误用例,报错信息为a为定义
printf("%d\n", new1::a);//正确用例
return 0;
}
为什么说定义的变量在全局中,但是不可以直接使用呢?举个例子,现实生活中拿东西,你得先看到物品在哪,才能去拿。如果说这个物品隐身了,你看不到它,自然无法拿到。namespace定义了new1的命名空间,这个空间在全局中,但是new1中的空间内容默认是不可见的。new1::a中的"::"是作用域限定符,左边是作用域名称(全局域就不用写),右边是成员名称。作用域限定符相当于帮你拿到你看不到空间中存在的物品。这样子可以避免命名冲突,只要存在的命名空间不一样,就没有问题。
2.在一个工程中可以存在多个名字相同的命名空间
#include<stdio.h>
namespace new1
{
int a = 10;
}
namespace new1
{
int b = 10;
}
int main()
{
printf("%d\n", new1::a);
printf("%d\n", new1::b);
return 0;
}
这因为编译器最后会对名字相同的命名空间进行合并。
3.命名空间可以嵌套
#include<stdio.h>
namespace A
{
int test1 = 10;
namespace B
{
int test2 = 20;
}
}
int main()
{
printf("%d\n", A::test1);
printf("%d\n", A::B::test2);
return 0;
}
命名空间嵌套定义,在使用时一层一层解开,如声明代码中的test2变量,A::B::test2可以看做A::(B::test2),和俄罗斯套娃一样。
4.using剥掉命名空间隐身衣
有人觉得自己就做个题,不想每次都用作用域限定符写。那可以直接用using将想要用的命名空间的隐身衣剥去,让它暴露在人们的视野里。
#include<stdio.h>
namespace A
{
int test1 = 10;
namespace B
{
int test2 = 20;
}
}
using namespace A;//将命名空间A的内容暴露在当前作用域内
using namespace B;//将命名空间B的内容暴露在当前作用域内
//如果只想要B中的内容,可以 using namespace A::B;
int main()
{
printf("%d\n", test1);
printf("%d\n", test2);
return 0;
}
有人又觉得自己只需要其中的某个变量或者函数等等,可以using A::test1,之后就可以直接使用test1变量了。个人推荐后一种方法,前一种虽然方便,但是潜在风险比较大。namespace本身就是降低命名冲突问题的,using可以让每个人更自由的去使用命名空间中的内容,namespace相当于主力军,using相当于辅助,大多数情况下,不会有人让辅助去当主力军的。
三、输入输出
#include<iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
int a;
cin >> a;
cout << "hello world!"<<endl <<"hello doudou!"<<endl;
return 0;
}
cout是标准输出对象(控制台),cin是标准输入对象(键盘输入)。cin和cout后面的>> 和 << 分别是流提取运算符和流输入运算符,endl相当于换行。cin、cout、endl都在iostream库中的std命名空间中定义。正常情况头文件都有带.h,为什么C++的iostream库没有.h后缀,主要是和C中的iostream.h库做区分。可以看出iostream库中有命名空间std的存在,而C中没有命名空间这个概念,当然有些旧版的C++编译器仍然支持iostream.h格式。
有人说cout、cin比printf和scanf慢,在早些时候这个差距会比较明显一些,但是随着机械部件不断迭代更新,这种差距基本上忽略不计。用哪种方法看个人习惯或者具体运用事例,哪种方便就用哪种。
四、缺省参数
缺省参数就是给函数参数给定一个默认数值,当调用函数时有传入数值,就正常进行传值操作,没有就采用默认值。
缺省参数又分为全缺省参数和半缺省参数
1.全缺省参数
#include<iostream>
using namespace std;//测试用例就偷个懒,不建议做项目这样写
//全缺省参数
void f(int a = 1, int b = 2, int c = 3)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
f();
f(10);
f(10, 20);
f(10, 20, 40);
return 0;
}
顾名思义,就是所有参数都是缺省参数。调用可以传入数值,如果有传入数值,必须按顺序传。如上面代码中的f函数,不可以用f( , 20,);这样的方式调用函数,想要传入某个参数,其前面的所有参数都需要传入数值,后面的可以不用。
2.半缺省参数
半缺省参数是指函数有一个或者一个以上的缺省参数,且没有全部参数都为缺省参数。
#include<iostream>
using namespace std;//测试用例就偷个懒,不建议做项目这样写
//半缺省参数
void f(int a, int b = 2, int c = 3)//半缺省参数函数
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
//void f(int a, int b, int c = 3)//半缺省参数函数
//{
// cout << a << endl;
// cout << b << endl;
// cout << c << endl;
//}
int main()
{
f();
f(10);
f(10, 20);
f(10, 20, 40);
return 0;
}
半缺省参数函数定义时,只能按顺序定义,函数定义的缺省参数后面必须都是缺省参数。半缺省参数函数调用方式和全缺省类似,没有缺省参数的地方依旧正常传数值,有缺省参数的地方,依旧是想要传入的参数,其前面的所有参数都需要传入数值,后面的可以不用。
五、函数重载
C++中支持同个作用域内可以声明定义多个名字相同功能相识的函数,当然是有规则的。现在就要讲讲什么时候可以定义名字相同的函数,且能被识别出来。主要三个方面,第一个是函数参数个数不同,第二个是函数参数类型不同,第三个是函数参数顺序不同。
这三个规则通过代码的修饰展现:
//1.参数类型不同
int Add(int a, int b)
{
cout << "int Add(int a, int b)" << endl;
return a + b;
}
double Add(double a, double b)
{
cout << "double Add(double a, double b)" << endl;
return a + b;
}
//2.参数个数不同
void Number(int a)
{
cout << "void Number(int a)" << endl;
}
void Number(int a, int b)
{
cout << "void Number(int a, int b)" << endl;
}
//3.参数顺序不同
void Order(int a, char b)
{
cout << "void Number(int a, char b)" << endl;
}
void Order(char b, int a)
{
cout << "void Number(char b, int a)" << endl;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
Number(1);
Number(1, 2);
Order(1, 'a');
Order('a', 2);
return 0;
}
这三个规则一定在什么时候都适用吗?真要细究不一定,这主要是第三个会有些问题,例如我定义一个函数中的参数类型都是一样的,就算参数的顺序不同,依旧会报错。为什么会这样?我们得去为什么可以实现函数重载。C语言中没有函数重载的概念,在C++中为什么有,因为编译器对于函数的命名规则不一样。这边先补充一个知识点,一个项目的文件中的程序是如何运行起来的。首先需要经过预编译,然后是编译,在之后是汇编,最后就是链接。预编译是处理一些预编译指令的,编译就是经过编译器生成一串串的汇编代码,汇编就是将这些汇编代码转化二进制指令,形成符号表。链接就是合并这个项目中的所有符号表。就是将这些这里不深讲,大致讲个过程,后面会单独出一章讲解这些过程中的细节。
编译器负责的就是编译的过程。在编译过程中,C语言的函数是直接用函数名修饰的,也就是说两个同样函数名的函数,即使参数不一样,C语言也分辨不出来。Liunx下的C++编译器对于函数名的修饰规则是_Z+函数长度+函数名+参数类型首字母。可以看出同名函数想要区分得靠最后一个参数类型做区分,这里也可以看出为什么类型相同的参数,调换顺序也无法识别出来,因为类型相同无法区分。上面三条规则为什么能适用,哪里会存在问题,记住这条规则便可。
六、引用
引用就相当于是给一个人或则一个东西取了一个别名。就像棍子,有的人也叫它棒子,东西还是同一个,名字不同罢了。那引用也一样,就是给一个变量多了一个变量名称,地址都一样,不开辟新空间。
1.引用的使用方法
想使用,得先定义吧,那就先看一下如何定义引用。
#include<iostream>
int main()
{
int a = 10;
int& b = a;
printf("a = %d, b = %d\n", a, b);
printf("a = %p, b = %p\n", &a, &b);
return 0;
}
定义方法就是:类型 & 引用名 = 引用变量名;
那有人就说了,我不初始化不行吗?我就定义一个引用不行吗?不行,一定要初始化,不然编译不过。语法规定好的,别去钻牛角尖。
一个人有多个外号可以吗?当然可以,那引用也一样,一个变量可以有多个引用名,对引用名进行引用,本质上还是对同一个问题取外号,都是指向同一个变量。
下面是代码验证:
#include<iostream>
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = c;
printf("a = %d, b = %d, c = %d, d =%d\n", a, b, c, d);
return 0;
}
引用需要注意的一点,一旦对一个变量进行引用就不可以将这个引用名转到另一个变量上。例如我给a取了引用名为c,那c就只能是a的引用名,不可以把它变成b的引用名。所以,引用一旦完成,就会从一而终。和指针这个渣男不一样,可以换对象。
2.常引用
常引用就是通过const对引用进行修饰。
下面是使用案例:
#include<iostream>
int main()
{
const int a = 10;
//int& b = a;//a为常量,权限不同报错
//int& b = 10;//10为常量,报错
const int& a1 = a;
const int& b = 10;
double c = 1.22;
//int& d = c;//报错,类型不同
const int& d = c;
printf("%f", d);
return 0;
}
上面举了几个典型例子,现在来讲解一下为了有些地方不行。常变量a是只能读不能修改的,但是主函数第二行的引用名b是既能读又能改的,两者权限不一样。权限可以缩小,但是不能放大。如果对常变量a取一个变量类型的别名b,那相当于是放大了a的权限,因为b是可以对a进行修改的,a和b就是同一个东西,所以不可以通过,合情合理。第三行也是同样的道理,10是常量,b是变量,常量的权限小于变量,不可以通过。
那第四行和第五行为什么可以通过,就是大家权限相同。
第八行不能通过是因为d和c的类型不一样,所以报错。当然有人就会说那第二行和第三行就不能是类型不一样吗?我只能说每个人的理解不一样,你就得可以都解释的通就行。
第九行就是反驳点了,你说类型不一样就会报错,为什么第9行不报错?这就是我前面第二三行为什么要说是权限问题报错,变量为什么可以给常变量,就是权限大的可以缩小,变量的权限比常变量大,能通过合情合理。但是这里会有个问题,d是常变量只能看不能改,但是c是变量啊,它能改,这有问题,还是不对。确实有问题,但是这个问题有个巧妙的地方让这个问题消失了。当d给c做引用时,其实给的不是c,给的是c的整数,也就是说c是1.22,给到b的其实是1,这个1是一个临时变量的值。也就是说,c先将其整数值给到临时变量,再将这个临时变量给到d。那d和c是没有半毛钱关系的,后面修改c不会改变d的值,引用始终如一。
3.为什么使用引用?
有人觉得引用可以做到的事情,指针不一样行吗?确实如此,引用的产生是为了将一些指针能做的事情做的更方便,因为指针过于复杂,引用相当于一个平替,可以完成大多数指针可以完成的事情,用起来简单。像链表就不能用引用去完成,引用没有指针灵活。
引用主要应用在两个地方,一个是函数参数,一个是函数返回值。我们先说使用方法,再谈为什么这样做。
#include<stdio.h>
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int& A()
{
int d = 30;
int& c = d;
return c;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
printf("a = %d b = %d\n", a, b);
int c = A();
printf("c = %d\n", c);
return 0;
}
上面代码在有些环境下是可以通过的,这不代表代码没问题,先声明一点,上面代码是存在问题的。函数参数使用引用是没有问题的,可以很方便的使用,并且不会对参数进行拷贝,在数据量很大的情况下,可以有明显的效率提高。举个例子,我传一个10万字节的结构体,如果用传值调用,在函数调用建立函数栈帧时,函数得开辟一个10万字节的空间对这个结构体进行拷贝,这里存在时间的消耗是很明显的。
函数返回值的代码是有问题的,下面就具体讲讲这个问题是什么。首先要明确的一点是,函数在调用时,会建立函数栈帧,函数结束后,函数栈帧会销毁。上面将函数A中的d变量作为返回值给了c,乍一看好像是这样。实际上,c是拿到了函数A中b变量空间的引用,换句话将,本质上c变量得到的是b的地址中的内容的引用名。这里面就存在隐患了,因为函数结束后函数栈帧是会销毁的,函数栈帧相当于是系统临时给函数分配的空间,一旦函数结束,这片空间就会回收,有的系统会对空间内容进行清理,有的系统不会,这就是为什么说有的环境下可以通过这个代码的原因。这时候就有人说了,那我知道了我的系统不会对销毁的函数空间进行清理,那我不就可以正常使用了。
片面了,这也是为什么要说函数栈帧的原因,因为函数栈帧销毁后,空间会被系统回收,而这块回收的空间又在别的函数建立时被占用。函数栈帧销毁,空间是会被系统回收的,也就是说这个空间的内容不可以随意被访问,没有权限。能拿到只是没被检查出来,实际上就是违法访问空间。如果说,别的函数使用了这块空间,那c和一个会被随时会被更改的空间建立了引用关系,这就违背了作者的初心,c的值也会变幻莫测。
关键点:函数返回值做引用正确使用方法是什么?返回对象出了作用域还在,就可以使用。如果空间归还给系统,就不推荐这样使用。
如有不当之处,感谢各位大佬指正。