一、命名空间
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全 局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名 冲突或名字污染,namespace关键字的出现就是针对这种问题的。
1.1 命名空间的价值
c语言项目普遍存在着命名冲突的问题,C++引入namespace就是为了更好的解决这一问题。
我们来看下面的代码:
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
如果我们编译上面的代码,编译器会给我们报一个错误:"rand": 重定义;以前的定义是"函数"
为了解决这样的问题,在C++中引入了命名空间。
2.2 命名空间的定义
定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中
即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
1.命名空间的普通定义:
cpp
namespace zj
{
int a;
double b;
int add(int x, int y)
{
return x + y;
}
}
这就是命名空间的普通定义,在命名空间内既可以定义变量也可以定义函数,zj是命名空间的名字。
2.命名空间的嵌套定义:
cpp
namespace zj
{
int a;
double b;
int add(int x, int y)
{
return x + y;
}
namespace zhangsan
{
int x = 0;
int add(int x, int y)
{
return (x + y) * 10;
}
}
}
命名空间只能定义在全局,但命名空间是可以嵌套的,在一个命名空间内输入另一个命名空间。
3.项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
cpp
namespace zj
{
int a;
double b;
}
namespace zj
{
int add(int x, int y)
{
return x + y;
}
}
编译器最终会合成到一个命名空间中。
2.3 命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
- 指定命名空间访问,项⽬中推荐这种⽅式。
- using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
- 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
cpp
#include<stdio.h>
namespace zj
{
int a = 0;
int b = 1;
}
int main()
{
printf("%d\n", a);
return 0;
}
我们在执行上述代码时,会发生报错,显示a是未声明的标识符。
这其中的原因在于编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找,所以我们查找不到命名空间里的a变量,导致了这个报错。
下面我们来讲讲命名空间的三种使用方法:
1.指定命名空间访问
先看下列代码:
cpp
#include<stdio.h>
namespace zj
{
int a = 0;
int b = 1;
}
namespace zhangsan
{
int a = 1;
int b = 2;
}
int main()
{
printf("%d\n", zj::a);
printf("%d\n", zhangsan::a);
return 0;
}
在上述代码中我创建了两个命名空间,一个是zj一个是zhangsan,他们中都创建了两个变量,我们可以使用::来访问指定的命名空间,像zj::a就是访问zj命名空间的a变量。
结果:
2. using将命名空间中某个成员展开
先看下列代码:
cpp
#include<stdio.h>
namespace zj
{
int a = 0;
int b = 1;
}
namespace zhangsan
{
int a = 1;
int b = 2;
}
using zhangsan::a;
int main()
{
printf("zj::a=%d\n", zj::a);
printf("zhangsan::a=%d\n", a);
return 0;
}
与第一种方法不同的是,我们增加了using zhangsan::a,他的意思是将zhangsan命名空间中的a成员展开,这样我们便能在全局中查找到zhangsan命名空间的a变量,所以我们将第二个printf的输出修改为a,结果与第一种方法是一样的。
结果:
3.展开命名空间中全部成员
先看下列代码:
cpp
#include<stdio.h>
namespace zj
{
int a = 0;
int b = 1;
}
namespace zhangsan
{
int a = 1;
int b = 2;
}
using namespace zhangsan;
int main()
{
printf("zj::a=%d\n", zj::a);
printf("zhangsan::a=%d\n", a);
printf("zhangsan::b=%d\n", b);
return 0;
}
与第二种方法只将命名空间内的某一个成员展开不同,第三种方法是将命名空间全部展开,像上述代码便将命名空间zhangsan全部展开,所以我们在全局中能找到命名空间zhangsan的a、b变量,所以我们在printf上输出的a、b变量其实就是命名空间zhangsan内的变量a、b.
结果:
虽然第三种方法能使得命名空间全部展开,但是这种方法却有很大的弊端,如果我们在命名空间zhangsan内加入一个变量为printf,那么这个程序便会报错。
加入了printf变量后,便于输出函数printf重名了,导致编译器识别不出来printf是输出函数还是常量printf,就会报错,所以这种方法是是有很大弊端的,如果你将两个或以上的命名空间同时展开,而且这些命名空间里又有重名的变量,便会报错,所以不推荐使用这种方法。
二、C++的输入输出
C++的输入输出函数包含在头文件<iostream>中,<iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
相比与C语言的输入输出函数,C++的输入输出函数要更加高级,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输入输出可以⾃动识别变量类型,其实最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
在C++中,cout是输出函数,cin是输入函数,endl是换行,这三个函数都属于C++标准库,而C++的标准库都存放在命名空间std中,所以我们要通过命名空间的使用方式去使用他们,一般在日常生活的练习当中,为了寻求方便,我们一般会将整个命名空间std全部展开,当然也仅限于日常练习,在其他时候还是不要轻易的将命名空间全部展开,前面已经说过了他的弊端。
下面我们通过一段简单的代码来了解C++的输入输出。
cpp
int main()
{
int a;
double b;
cin >> a;
cin >> b;
cout << "" << a << " " << b << endl;
return 0;
}
结果:
三、缺省参数
缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参
则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。
下面我们来看一个简单的例子:
cpp
void func(int x = 20)
{
cout << "" << x << endl;
}
int main()
{
func();
func(10);
return 0;
}
函数的参数int x=20就是缺省参数,在调用函数时,如果没有传递参数,那么就会采用缺省参数,传递了参数就要所传递的参数。
结果:
可以看到,在未传递参数时,输出的值为缺省参数20,传递参数时,输出的便是所传递的参数。
3.1 全缺省
全缺省就是全部形参给缺省值。
cpp
void func(int x = 20,int y=30,int z=40)
{
cout << "" << x+y+z << endl;
}
int main()
{
func();
func(10);
func(10, 20);
func(10, 20, 30);
return 0;
}
如上图代码所示,函数func就是全缺省函数。
结果:
3.2 半缺省
半缺省就是部分形参给缺省值C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
cpp
void func(int x ,int y=30,int z=40)
{
cout << "" << x+y+z << endl;
}
int main()
{
func(10);
func(10, 20);
func(10, 20, 30);
return 0;
}
上图所示的函数func便是半缺省函数,因为半缺省函数不能间隔跳跃给缺省值,所以像这种形式void func(int x ,int y=30,int z)的半缺省便是错的。
结果:
注意:函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值
四、函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。而 C语⾔是不⽀持同⼀作⽤域中出现同 名函数的。
4.1 参数类型不同
我们来看下面的代码:
cpp
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;
}
int main()
{
int ret1=Add(3, 5);
cout << "" << ret1 << endl;
double ret2=Add(3.4, 6.6);
cout << "" << ret2 << endl;
return 0;
}
这便是参数类型不同的函数重载。
结果:
4.2 参数个数不同
来看下面的代码:
cpp
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(10);
return 0;
}
这便是参数个数不同的函数重载。
结果:
4.3 参数类型顺序不同
来看下面的代码:
cpp
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;
}
int main()
{
f(3, 'a');
f('a', 3);
return 0;
}
这便是参数类型顺序不同的函数重载。
结果:
五、引用
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋风";林冲,外号豹⼦头;
类型& 引⽤别名 = 引⽤对象;
cpp
int main()
{
int b = 10;
int& c = b;
cout << "" << &b << endl;
cout << "" << &c << endl;
}
上述代码便是引用,相当于给b取了个别名叫c,输出的结果我们能看到b和c的地址应该是一样的。
结果:
5.1 引用的特性
- 引⽤在定义时必须初始化
- ⼀个变量可以有多个引⽤
- 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
- 引⽤在定义时必须初始化
不初始化的话编译器会报错。
2.⼀个变量可以有多个引⽤
可以看见,对于a的其他引用的地址都是a的地址。
- 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
b是a的引用,c的值为20,b=c其实是将c的值赋给b,而不是将b改为c的引用,所以最后a的值被改为了20。
5.2 引用的使用
1.引用做参数
引用做参数就和指针做参数一样,形参可以影响到实参,我们用最经典的swap函数来验证一下。
cpp
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
这就代表着以后我们想要让形参影响实参,可以不再传指针,而是可以传引用。
5.3 const引用
可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访
问权限在引⽤过程中可以缩小,但是不能放⼤。
下面我来距离几种const引用比较容易出错的地方:
这时第一种可能出错的情况,这里的引用是对a访问权限的放大,在int前加个const就好。
这时第二种可能出现的情况,因为const修饰的常量,所以我们不能对常量做任何修改。
cpp
int main()
{
const int a = 10;
const int& ra = a;
int b = 20;
const int& rb = b;
return 0;
}
这两种对const的引用才是正确的。
六、引用与指针的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
- 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
- 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
- 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
- sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
- 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
七、内联函数
⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率。
C++设计inline的目的就是为了替代C语言的宏函数,因为C语言的宏函数是很复杂且很容易出错的。
这里要注意的是, inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展 开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁 调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
那么该如何判断内联函数是否被展开了呢,我们可以通过以下代码来判断:
cpp
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}
这时我们通过调试进到反汇编中,如果在反汇编中,发现有call Add语句就是没有展开,否则就是展开了
我们可以看到,里面的指令有call且有Add函数,所以内联函数Add是没有被展开的,大家也可以删减一下代码再去看看是否被展开。
八、nullptr
nullptr是C++中的空指针,与C语言的空指针NULL不同,在C++中,NULL代表的是0。
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
所以在C++中,NULL会被定义为字面常量0,所以C++
引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
我们通过下面的代码来验证一下:
cpp
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}
结果:
由此可见,0和NULL都被当做是常量,将NULL强制转换为int*也变成了指针,所以(int*)NULL和nullptr都是指针。
九、总结
上面所讲的都是C++一些入门的基础知识,如果觉得有帮助的话,别忘了一键三连,谢谢各位。