目录
一、命名空间
二、缺省参数
三、函数重载
四、extern C
五、引用
前言
在学习C++过程中,网络上很多资料和视频中都直接告诉我们要在引完头文件之后加上using namespace std; 很多时候学到最后也没有弄清楚为什么要加上这一条语句,这实际上就是C++的命名空间。
名字空间(英语:Namespace),也称命名空间、名称空间等,它表示着一个标识符(identifier)的可见范围。一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中。
例如,设Bill是X公司的员工,工号为123,而John是Y公司的员工,工号也是123。由于两人在不同的公司工作,可以使用相同的工号来标识而不会造成混乱,这里每个公司就表示一个独立的名字空间。如果两人在同一家公司工作,其工号就不能相同了,否则在支付工资时便会发生混乱。
这一特点是使用名字空间的主要理由。在大型的计算机程序或文档中,往往会出现数百或数千个标识符。名字空间提供一隐藏区域标识符的机制。[1]通过将逻辑上相关的标识符组织成相应的名字空间,可使整个系统更加模块化。
在编程语言中,名字空间是对作用域的一种特殊的抽象,它包含了处于该作用域内的标识符,且本身也用一个标识符来表示,这样便将一系列在逻辑上相关的标识符用一个标识符组织了起来。许多现代编程语言都支持名字空间。在一些编程语言(例如C++和Python)中,名字空间本身的标识符也属于一个外层的名字空间,也即名字空间可以嵌套,构成一个名字空间树,树根则是无名的全局名字空间。
函数和类的作用域可被视作隐式名字空间,它们和可见性、可访问性和对象生命周期不可分割的联系在一起。
#include
int main()
{
int scanf = 10;
int strlen = 10;
scanf("%d", &scanf);
return 0;
}
以这样的代码为例:首先定义一个int型变量scanf,然后调用sacnf来从控制台获取数据,这时就会出现问题
scanf是C语言中的库函数,可是我们一不小心定义的变量或者函数名称与库中函数名相同了,这是十分尴尬的情况,在平时的工程中我们经常出现这种情况,C++为了应对C语言这样定义变量不方便的情况,加入了命名空间来解决。
C++用命名空间将名字隔离
#include
namespace C
{
int scanf = 10;
}
int main()
{
scanf("%d", &C::scanf);
}
编译成功
由此我们就可以对标识符的名称进行本地化,以避免名称冲突或名字污染。
而我们定义的命名空间也就是一个作用域
在我们没有指定作用域时编译器会默认在main这个局部域或者全局域中寻找
域中是可以定义函数的,同时命名空间可以嵌套定义命名空间,在同一个工程中是允许存在多个相同名称的命名空间,编译器会将其合成到同一个命名空间中,在不同域中可以定义同名空间。
这时我们回到最初的问题,我们为什么要加入using namespace std;?
这是因为C++库为了防止命名冲突,把自己库里面的函数和变量都定义在std命名空间中;同时细心的小伙伴注意到了C++的头文件没有了.h 这是为了与C语言的库进行区分,最初版本的C++库是没有命名空间的因此某些资料上会出现#include
这里解释了std的由来
再然后是,如何从命名空间中读取函数和变量等
一共有3种获取方法
1、每一个变量指定命名空间———这种方法比较麻烦,每一个地方都要指定作用域,这个最规范的形式 例如 std::cin
这是没有加上作用域的情况
2、把std整个展开,相当于将库里面的全部东西都展开到全局域,如果自己定义的东西与库里面的发生命名冲突,就会无法解决,一夜回到解放前,与没有命名空间的C语言一样了
这种方式适合与日常练习 使用using namespace std;
3、对部分常用库里面的东西展开
例如 using std::cout;
using std::cin;
在程序设计中,一个函数的缺省参数是指不必须指定值的参数。在大多数程序设计语言中,函数可以接受一个或多个参数。通常对于每个参数都需要指定它们的值(例如C语言[1])。一些较新的程序设计语言(例如C++)允许程序员设定缺省参数并指定默认值,当调用该函数并未指定值时,该缺省参数将为缺省值。
#include
using namespace std;
//using std::cin;
//using std::cout;
//using std::endl;
void func(int n = 0)
{
cout << n << endl;
}
int main()
{
int n = 20;
//cin >> n;
//cout << n << endl;
func();
func(n);
func(120);
return 0;
}
没有参数时,使用的是默认值
传参时,使用指定的实参
我们要注意以下几点
1、半缺省参数必须从右向左依次来给出,不能间隔着给
2、缺省参数不能在函数声明和定义中同时出现
3、缺省值必须是常量或者全局变量
4、C语言编译器不支持
缺省参数能够使调用变的更加灵活
半缺省就是指 函数如果有多个参数,我们从右向左将部分参数赋初始值,不是指将一半的参数赋初始值,缺省参数可以在类的构造函数中使用
函数重载(英语:function overloading)或方法重载,是某些编程语言(如 C++、C#、Java、Swift、Kotlin 等)具有的一项特性,该特性允许创建多个具有不同实现的同名函数。对重载函数的调用会运行其适用于调用上下文的具体实现,即允许一个函数调用根据上下文执行不同的任务。
例如,
doTask()
和doTask(object o)
是重载函数。调用后者,必须传入一个object
作为参数,而调用前者时则不需要参数。一个常见的错误是在第二个函数中为object
分配一个缺省值,这将会导致意义模糊的调用错误,因为编译器不知道使用这两种方法中的哪一种。另一个例子是
Print(object o)
函数,它根据是打印文本还是照片来执行不同的操作。这两个不同的功能可以重载为Print(text_object T); Print(image_object P)
。如果我们为程序中将要“打印”的所有对象编写重载的打印函数,就不必担心object
的类型,再次调用相应的函数,调用始终是:Print(something)
。
在C语言阶段,我们经常要写一些函数,例如Add() Sub()之类的函数,但是因为要做加法和减法的变量类型不同,我们要写很多具有类似功能的函数,调用时也要格外小心。十分的不方便,C++给出了函数重载的特性,可以减轻我们的工作量
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
int main()
{
int n = 20;
//cin >> n;
//cout << n << endl;
//func();
//func(n);
//func(120);
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
return 0;
}
我们写了两个同名的函数Add()但是这两个函数的参数不同,我们传入的参数类型不同,编译器就会自动调用不同参数的函数来完成操作。
函数重载也有几个要注意的点
1、函数名称相同且在同一个作用域中
2、 函数参数不同,参数类型不同,或者参数的个数不同
3、函数重载与返回值无关
以上几点注意会在下面的问题中解答。
根据我们上面的缺省参数的概念如果我们定义这两个函数会出现什么现象呢?
void f(int a, int b, int c = 1)
{
}
void f(int a, int b)
{
}
我们发现编译没有出错,说明编译器支持这样的函数重载和缺省参数的共同使用,但是我们要是这样调用函数就会出错
我们这样调用会出现函数调用不明确的情况
因此,我们在利用函数重载时,最好不要使用缺省参数
但是下面的函数重载则是合法的
int Add(int x, int y)
{
return x + y;
}
long Add(long x, long y)
{
return x + y;
}
int main()
{
int n = 20;
//cin >> n;
//cout << n << endl;
//func();
//func(n);
//func(120);
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
//f(1, 2);
f(1, 2, 3);
cout << Add(1, 2) << endl;
cout << Add(2, 9) << endl;
return 0;
}
这是调用的是哪一个Add函数呢?
如果我们要指定调用带有两个Long类型的Add()函数,可以使用这样的调用方法
cout << Add(1, 2) << endl;
cout << Add(2L, 9L) << endl;
这样就能够指定调用重载的函数了。
函数重载的意义在于让用的地方很方便,就像使用同一个函数一样
例如,我们经常使用的cin 和 cout 就是利用函数重载来实现的,它可以自动识别变量的类型,而不像C语言一样scanf 和printf 要记住它的格式化参数%d %s %ld等等
这里引出了两个问题
1、为什么C语言不支持重载,C++底层如何支持重载?
2、extern "C"的作用是什么?
我们知道C/C++编译器在编译链接有以下几个步骤
我们定义三个文件分别为 func.h(头文件,函数声明) func.cpp(函数定义) main.cpp(main函数)
1、预处理——头文件的展开——宏替换——去掉注释——条件编译
func.i main.i
2、编译——语法检查,生成汇编指令
func.s main.s
3、汇编——把汇编代码转换成二进制机器码
fumc.o main.o
4、链接——将上述文件链接到一起
a.out
我们C语言一般是在链接阶段发现函数的重定义的,C++的解决方法是添加了一个函数名修饰规则
我们知道汇编时会生成符号表,C语言是在符号表中存入函数的地址,然后在(链接过程中调用函数的文件中储存的是函数名)去其它文件中寻找,如果找不到,就会出现链接错误
而C++的函数名修饰规则可以将函数的真实名称隐藏掉例如我们前面写过的Add()函数,它经过C++编译器处理过之后的名称是<_Z3addii>这是在Linux系统下的符号修饰。
我们解析一下这个符号,_Z3是指函数名有三个字符 ,add是指函数名,ii是函数两个参数类型的简写ii就是两个int型参数
链接时会将函数的地址填入到
在这里,我们也就知道了函数重载要遵循的几个要点,正好是将函数名处理之后剩下的部分
在用C++的项目源码中,经常会不可避免的会看到下面的代码:
1 2 3 4 5 6 7 8 9 |
|
它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试or笔试中。
在一些大项目中,我们会出现C++的编译器去调用C的库函数,C去调用C++的库函数
这时我们就需要使用extern c
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,
将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree
两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决
我们会添加一个叫做中间件的程序,通常将它编译成静态库或者动态库
中间件程序,C语言程序可以调用,C++程序也可以调用
这种中间件程序是利用C++来写的,因为C语言是不支持extren C的
我们首先要明确几个要点:C++是调不动C语言写的程序的,同样的C语言也无法调动C++程序
所以才会出现extren C
原因是C++在链接时会将函数名处理,而C语言编译器是无法找到函数名的,因为C语言是直接用函数名来找的,所以链接报错,而C语言在链接时也是同理,用extren c来使编译器按照C语言风格来编译;
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名 = 引用实体
#include
using namespace std;
int main()
{
int a = 20;
int& b = a;
printf("%p\n", a);
printf("%p\n", b);
return 0;
}
b是a的引用,我们打印出这两个变量的地址
b是a的引用,我们打印出这两个变量的地址,发现它们是相同的,这也就从侧面印证出,b是a的别名,从语法的角度分析,
它们就是一个变量,并且引用和变量共用同一块内存空间
注意:引用类型必须和引用实体是同种类型的
引用特性:
1、引用在定义时必须初始化
2、一个变量可以有多个引用
3、引用是变量的别名,不可以给NULL定义引用
#include
using namespace std;
int main()
{
int a = 20;
int& b = a;
int c = 200;
/*printf("%p\n", &a);
printf("%p\n", &b);*/
b = c;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
b = c 这条语句是定义b是c的别名吗?
答案是显然错误的:引用一旦引用一个实体就不能引用其它实体,这是相当于把c的值赋值给b
,既然赋值给b了也就相当于赋值给了a
引用的适用场景
1、做参数
我们在写C语言时,经常要写例如交换两个变量的函数,要传入两个变量的地址
这时我们可以传入两个变量的引用
#include
using namespace std;
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 20;
int& b = a;
int c = 200;
/*printf("%p\n", &a);
printf("%p\n", &b);*/
//b = c;
printf("%d\n", a);
printf("%d\n", c);
swap(&a, &c);
printf("%d\n", a);
printf("%d\n", c);
swap(a, c);
printf("%d\n", a);
printf("%d\n", c);
return 0;
}
我们以两种方式定义swap函数,一种是传入指针,另一种是传入引用
因为一个swap函数参数是int*类型,另一个swap函数参数是int型,两者构成重载,所以编译不会报错。
我们先将a和c交换,在将a和c交换回来
传入的是指针或者引用,这种类型的参数是输出型参数
2、做返回值
在研究引用做返回值之前,要先研究明白另一件事
我们再以另一个函数为例
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 20;
int b = 30;
printf("%d\n", Max(a, b));
return 0;
}
这个函数返回的是两者较大的一个值,这种类型的叫做传值返回
返回的是较大值的临时变量的拷贝
直接传值返回时,不是返回Max中的最大值,而是最大值的临时拷贝,这个临时拷贝一定不存在Max函数栈帧中,因为函数调用结束,再次访问这个函数的栈帧中的值是非法访问。
以上面代码为例,这个最大值的临时拷贝存在于main函数栈帧中或者直接储存在寄存器中
也就是说只要是函数传参的方式是传值
并且带有返回值的函数,返回的都是返回值的临时拷贝
我们这时用传引用函数举例:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
虽然成功运行,但实际上,它已经越界了
我们还要明确一个点:越界是不一定报错的,一般在函数结束时才会检查是否越界
操作系统对于越界的检查是抽查,操作系统是在容易越界的位置进行检查,在这些检查位,会设置一些特征值,如果我们因为越界将特征值修改, 则会报一个段错误
根据上面所述,Add函数返回的不是C,返回的是C的引用,C是一个局部变量,C出了作用域就已经销毁了,这时返回一个临时变量的引用,就会造成非法访问
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = Count();
printf("%d\n", ret);
printf("%d\n", ret);
return 0;
}
这还是一个引用做返回值的例子:第一次打印的一定是1
第二次打印就会出现随机值
很显然,我们的推断是正确的
原因是:n是临时变量,函数结束就会被销毁,我们第一次打印出的是正确结果
因为第一次打印时:Count函数的栈空间没有被利用,栈中的数据不会被删除,它只会被覆盖,所谓的销毁,就是声明这块空间是不可用的
而第二次调用打印函数时,会建立函数栈帧,具体过程参考《函数栈帧的创建和销毁》建立栈帧之后会将Add函数之前的栈空间置为随机值,再次以十进制的方式格式化输出,那么结果必然是随机值
所以如果要返回出了函数就会销毁的的变量,一定不要使用引用作为返回值
我们只要经过简单的修改就可以解决这个问题
int& Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
//int ret = Add(1, 2);
/*int a = 20;
int b = 30;
printf("%d\n", Max(a, b));*/
int& ret = Count();
printf("%d\n", ret);
printf("%d\n", ret);
return 0;
}
这回结果就正确了
因为static定义的变量是储存在静态区的,出了作用域不会销毁 ,所以不会出现越界情况
总结:
1、如果函数返回时,出了函数作用域,返回对象还在,还没有被操作系统销毁就可以使用引用返回
2、指针和引用的区别:在语法上,引用是给这块空间起别名,没有开辟新空间,无论什么数据类型,地址都占用1个字节,就是开头的那一个字节,不论是数组还是什么其它的,指针类型表示能从开头地址向后访问的字节数。从汇编的角度看,引用的底层是类似指针储存地址的方式处理的。
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 20;
int b = 30;
swap(&a, &b);
swap(a, b);
return 0;
}
我们还是以swap函数这歌最基础的函数,查看它们两者汇编代码有什么区别
这是指针版本的swap的汇编代码
然后我们再查看引用版本的汇编代码
我们发现引用版本的汇编代码与指针版本的汇编代码一模一样,这也就证明两者的底层是类似的。
1. 引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
这也就说明指针更加强大,也更加危险!!!