目录
一、命名空间
1.概念
初接触C++的人在看到C++的代码时,都会在开头看到如下两串代码
#include<iostream>
using namespace std;
那我们都知道,在c中,#include一般是用来包含头文件的,在这里的这个#include也是一样的作用,用来包含<iostream>这个头文件,只是在c++中,为了与c相区别和正确使用命名空间,头文件的包含并不需要像c一样在后面加上.h。在c++中比较新的编译器已经不再支持头文件后加.h,只要比较老的编译器(如vc 6.0)才可能支持。
那么下面的using namesapce std;是什么意思呢?这就是c++中的命名空间。而std则是它所包含的一块取名为std的限定域。命名空间中可以包含变量、函数、结构体等各种各样的东西,而c++中出现命名空间的原因就来源于c语言的缺陷。
(1)流插入运算符和流提取运算符
这里补充一个知识点,就是我们在初见c++时,第一个接触的就是打印一个“hello world”,其中的打印代码与c完全不同
std::cout << "hello world" << std::endl;
在这里可以看到两种符号,<<和::。与<<相对的,还有一个>>
<<的名字是流插入运算符。简单来说就是将一串数据插入到某个函数之中。要注意,这行代码中的cout和endl其实都是函数。
::的名字是域限定符。就是限制数据到一个命名空间中去找相应的函数或变量。
>>则是流提取运算符。简单来讲就是从某处空间提取数据
int main(void)
{
int test = 0;
std::cin >> test;
return 0;
}
这行代码可以看成和c语言中的scanf()一样的作用
要注意的是,在上面的打印中末尾有一个endl,这个是c++标准库中的一个函数,可以看成‘\n’,即换行的作用。在使用时可以省略,如下所示:
2.命名空间出现的意义
(1)c语言的命名缺陷
用过c语言的人都知道,c语言中在同一个函数或同一个生命周期内都是不能出现相同的变量名和函数名的。这是因为当进行变量、函数调用的时候,都会遵循一个默认的查找规则,即先从局部找,再从全局找。先从局部找,就是指先从当前函数或生命周期内部开始查找,如果没找到,再到函数外部去找。
#include <stdlib.h>
int rand = 0;
int main(void)
{
printf("%d ", rand);
return 0;
}
就比如这几行代码,在这里定义一个全局变量rand,一眼看去没有问题,但是这里会造成函数的重定义。
我们都知道,同一工程中的头文件在编译时会将头文件展开,即将头文件里面包含的内容在该文件下全部展开。而在main()函数中调用rand时,因为在main()函数这个局部空间内没有找到rand,那么就会去函数之外的全局里面去找。而在找的时候头文件<stdlib.h>已经展开了,这个头文件中刚好有一个函数也叫rand,这就导致printf不知道该调用哪一个rand,导致了rand的重定义。
但有时候我们在给变量、函数命名的时候,并不知道我们包含的头文件中所有的函数名,就难免会造成命名的冲突,为了解决这一问题,c++便有了命名空间
(2)如何解决命名冲突
解决的方法很简单,因为查找规则是先从局部找,再去全局找。那么我们只需要让我们在调用的时候先去指定的局部区域查找即可。namesapce后面的是自定义的域名
namespace test
{
void rand()
{
std::cout << "hello world" << std::endl;
}
}
int main(void)
{
test::rand();
return 0;
}
3.命名空间的使用
命名空间分为c++官方库定义的命名空间和用户自己定义的命名空间,这两种空间的使用方法都是一样的
(1)不展开使用
以我们自己定义的命名空间为例,有以下命名空间
namespace test
{
void rand()
{
std::cout << "hello world" << std::endl;
}
}
我们在使用调用test限定域内的rand()函数时,需在调用前加上test::,如以下所示
int main(void)
{
test::rand();
return 0;
}
这种使用方式是不展开使用,即其他函数要调用该限定域内的东西时必须在前面加上域名,表示从该限定域中去寻找。
这种方式的好处是不会与外部函数名或变量名等产生冲突,坏处则是每次调用时都需要加上域名,比较麻烦
(2)完全展开使用
#include<iostream>
using namespace test;
完全展开使用方法是using namespace 域名;这种方式使用起来比较方便,在调用该限定域内的变量或函数时无需在前方加上域名,即如下表示:
namespace test
{
void sand()
{
std::cout << "hello world" << std::endl;
}
}
using namespace test;
int main(void)
{
sand();
return 0;
}
这种的好处时在调用限定域内的变量或函数时无需加上域名,但这样的做法和将函数或变量定义在全局是差不多的,因此如果与全局域中的函数名产生冲突时会造成重定义。
这种方式虽然在我们平时自己写小程序或刷题时会使用,但在大型的项目中尽量不要使用,因为可能与其他人写的函数名等形成冲突
(3)部分展开使用
部分展开,即将限定域中的函数部分放在全局域,这种方式一般都是用于频繁调用的函数或变量,多用在对官方库中的函数部分展开,展开的部分可以不加域名调用,其他部分则扔需加上域名。形式如下所示:
namespace test
{
void sand()
{
std::cout << "hello world" << std::endl;
}
}
using test::sand;
int main(void)
{
sand();
return 0;
}
这种方式即可以减少频繁的写域名,也可以减少与其他函数或变量产生冲突的可能性
(4)命名空间嵌套定义
和结构体相同,命名空间也可以嵌套定义。命名空间中包含各种事物,不仅仅是函数和变量,也包括其他命名空间。
namespace N1
{
int a = 0;
namespace N2
{
int b = 0;
}
}
而这类命名空间的使用方式也是需要在前面加入域名,如要调用N2中的b变量,则是N1::N2::b;
同时,因为在同一工程中包含的头文件在编译时会全部展开,那么在不同的文件汇合在一起时,也可能出现不同的命名空间拥有同一域名的情况。
这种情况下,相同域名的不同命名空间会合并。但是这种合并只会在同一层之间合并,不同层之间是无法合并的。
namespace N1
{
int a = 0;
namespace N2
{
int b = 0;
}
}
namespace N1
{
int c = 0;
namespace N2
{
int d = 0;
}
}
以上四个命名空间在编译时,会将两个N1合并在一起,再将N1中的两个N2合并。可以看成如下所示:
namespace N1
{
int a = 0;
int c = 0;
namespace N2
{
int b = 0;
int d = 0;
}
}
二、缺省参数
1.概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值,在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
简单来说,就是在函数的声明或定义中,可以给函数的形参一个指定的值,这样在调用该函数时如果没有在函数中输入值,则直接使用函数声明或定义中的值,形式如下:
#include <iostream>
using namespace std;
void f(int a = 10, int b = 20)
{
cout << a << '\n' << b << endl;
}
int main(void)
{
f();
return 0;
}
从上面的程序我们可以看到,在这里调用函数f()的时候,并没有输入传入值,但依然正常运行了。原因就在于函数定义中的f(int a = 10, int b = 20)。
在这里面我们传入了分别给形参a和b传入10和20,而我们也可以看到,在程序中成功的打印出了10和20两个数。这是因为我们在调用函数时并没有输出指定值,此时函数调用就会自动使用传入的缺省值
#include <iostream>
using namespace std;
int f(int a = 10, int b = 20)
{
cout << N1::a << '\n' << b << endl;
return 1;
}
int main(void)
{
f(1, 2);
return 0;
}
而在这段程序中我们可以看到当我们在函数调用时出传入了1和2两个数,那么打印出来的就是这两个数,这就证明了函数的缺省值的优先级是低于调用时传入的值的
2.缺省参数的使用
缺省参数的使用主要两个方式:全缺省和半缺省
(1)全缺省参数
全缺省参数指的是将函数中的所有形参都传值,即如下所示:
void f(int a = 10, int b = 20)
{
cout << a << '\n' << b << endl;
}
(2)半缺省参数
半缺省参数并不是真正意义上的一半参数加上缺省值,准确来说是局部参数加上缺省值
void f(int a, int b = 20, int c = 0)
{
cout << a << '\n' << b << endl;
}
当然,无论是半缺省还是全缺省在函数调用时都可以省略传参的过程。但要注意的是,半缺省参数不同于全缺省参数,在半缺省形式中传入了缺省值的参数的右边不能存在没有缺省值的参数
如
void test(int a = 10, int b, int c = 29)
{
cout << a << '\n' << b << endl;
}
这种形式编译器会直接报错,因为a传入了缺省值,但b没有
同理
void test(int a = 10, int b, int c)
{
cout << a << '\n' << b << endl;
}
这种形式也会报错。只要记住传缺省值时必须从右往左依次连续传入即可
注意:
(1)缺省参数不能在函数声明和定义中同时出现。
该行为是为了防止函数声明和定义中传入的缺省值不同而设定的。因为在写程序时,一般都会把函数声明放在一个头文件中,函数定义则放在另一个文件,因此如果两个都传入缺省值,就可能出现缺省值不同的情况。当需要传缺省值时,只需要在头文件中的函数声明中传入即可
(2)缺省参数传值只能传常量和全局变量
因为函数内部的变量出了自身的作用域就会被销毁,因此只能传常量和全局变量
三、重载函数
1.概念
在c语言中,是不允许同一个作用域内出现相同函数名的,因为在编译时无法识别调用的是哪个函数。但在写程序时,比如写一个交换函数swap(),如果都是需要交换两个变量的值,但变量的类型不同,为了防止命名冲突,只能每个都单独取一个名字。并且如果是多个人写的文件,在汇总时也可能出现函数名冲突的情况,为此,c++提出了重载函数的概念
函数重载是函数的一种特殊情况,c++中允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序不同),常用来实现功能类似数据类型不同的问题
2.c++重载函数可用原理
那我们都知道,程序在编译链接的时候会有一个符号表,这个符号表里面就存储了程序中的函数名及其对应的地址。
那为什么c语言中无法使用重载函数呢?因为c语言相对粗糙,在符号表中存储的函数名直接就是代码中的函数名,这样就导致了同名函数无法区分。
c++则不同,会根据函数带有的参数类型修改在符号表中的函数名,并根据调用函数时传入的值的类型去找到对应的函数
上图只是相对粗糙的展示的c和c++中函数名的存储形式,但可以看到,两者的函数名存储方式是不同的
但是要注意的是,重载函数只能根据函数的参数进行区分,无法用返回值进行区分
void test(int a, char b)
{
cout << a << '\n' << endl;
}
int test(int a, char b)
{
cout << a << '\n' << 'b' << endl;
return 1;
}
例如这两个函数,参数类型相同,返回值不同,但无法构成重载函数,会报错。
那我们都知道c++符号表中可以根据函数的参数修改变量名,那如果想用返回类型区分函数,同样可以在符号表中的函数名中加入一个指定值来完成,但实际上哪怕修改了符号表中的函数名也依然无法区分。
因为我们在调用函数时,只会输入传入的值,如果有返回值也只是用另一个变量去接受,而无法指定返回值类型。这就导致了如果是不同返回值的同名同参函数,在编译链接时缺少指定的返回值类型依然无法找到对应的函数,出现调用不明确的问题
3.重载函数使用
(1)参数个数不同
同名函数之间的参数个数不同
void test()
{
cout << 1 << '\n' << 2 << endl;
}
void test(int a)
{
cout << a << '\n' << endl;
}
void test(int a, int b)
{
cout << a << '\n' << b << endl;
}
(2)参数类型不同
同名函数之间的参数类型存在不同
void test(int a, char b)
{
cout << a << '\n' << endl;
}
void test(int a, int b)
{
cout << a << '\n' << b << endl;
}
(3)参数顺序不同
同名函数之间的参数顺序不同,但其本质也是参数类型不同
void test(int a, char b)
{
cout << a << '\n' << endl;
}
void test(char a, int b)
{
cout << a << '\n' << b << endl;
}
(4)重载函数与缺省参数混合使用
缺省参数可以在任意函数中使用,重载函数也不例外。当然在重载函数中也需要遵守缺省参数的使用规则。且在调用存在缺省参数的重载函数时,要注意尽量不要以可能出现函数调用不明确的方式调用。
如下面的三个重载函数,调用test()时没有输入值,此时又因为第一个没有参数,第二个和第三个函数的参数全部都有缺省值,test()函数可以调用这三个函数,导致函数调用不明确,报错。
test(1)虽然输入了一个值,避免了对test()的调用,但第二个和第三个函数中全部参数都有缺省值,test(1)既可以调用第二个函数,也可以调用第三个函数,导致函数调用不明确,报错
test(1, 2)输入了两个值,在这三个函数中只有第三个函数能够满足条件,调用成功
void test()
{
cout << 1 << '\n' << 2 << endl;
}
void test(int a = 10)
{
cout << a << '\n' << endl;
}
void test(int a = 10, int b = 20)
{
cout << a << '\n' << b << endl;
}
int main(void)
{
test();
test(1);
test(1, 2);
return 0;
}
四、引用
1.概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间
举个例子,在水浒传中有一名角色叫做宋江,这是他的本名。而在江湖上,宋江又有个外号叫做“及时雨”。无论是宋江还是“及时雨”,其实质上都是指的同一个人。而这种取别名的方式在c++中就叫做“引用”。
在c语言中存在指针,可以重新定义一个与变量同类型不同名的指针变量来指向另一个变量。而引用则是给一个变量取一个别名,指向的是同一个空间。一眼看去c语言中的指针似乎和c++中的引用是一个概念,但实际上并不是。其中一个不同点就是指针需要开辟空间存储地址,而引用则无需开辟内存空间,直接就是表示的对应的变量。
c++兼容c,因此在c++中也是可以使用指针的。而在c++中,有些功能只能使用指针完成,引用无法完成。但引用的使用却比指针简单的多,引用和指针各有优缺点,不需要纠结使用哪个更好,在使用时谁方便就使用谁
例如以前我们要将一个变量的值传入函数里面修改,必须要用到指针传地址,且使用的时候也必须要解引用,但有了引用后,就无需传地址,直接传值,在函数里面直接使用形参即可
void test(int& a)
{
cout << a << endl;
++a;
}
int main(void)
{
int a = 0;
test(a);
cout << a << endl;
return 0;
}
2.引用的注意事项
(1)引用和指针相同,引用的类型必须与被引用的变量类型相同
(2)引用在定义时必须初始化。
不同于普通变量如果没有初始化,那么存的就是随机值。引用因为是变量的别名,因此在定义时必须要初始化,如果不初始化则会报错
(3)一个变量可以有多个引用。
因为引用本质是给指定变量取了一个别名,而这个别名无论有多少个都行
int main(void)
{
int a = 0;
int& ra = a;
int& rra = ra;
int& x = a;
return 0;
}
在这里的ra,rra和x都是变量a的别名,无论对哪一个进行修改,都会导致a被修改。引用变量的引用对象既可以是变量本身,也可以是该变量的其他引用名
(3)引用一旦引用一个实体,再不能引用其他实体
不同于指针指向的对象可以随意修改,引用在作为一个变量的别名后就无法再次修改。哪怕修改成别的变量,也只会改变原变量的值而无法修改引用的对象
int main(void)
{
int a = 0;
int b = 10;
int& ra = a;
cout << ra << endl;
ra = b;
cout << ra << endl;
return 0;
}
(4)引用无法作为常量的别名
引用只能是变量的别名,而常量因为无法修改,因此不能对常量进行引用,包括普通常量和const修饰的变量
3.引用的使用场景
(1)做返回值
1.提高运行效率,减少拷贝
在前文中我们说过,引用有一个特点就是引用不会单独开一个内存空间存储,而是和引用的变量共用一块内存空间。
做返回值的用处就是建立在这个条件之上的,传引用返回的函数其运行效率都会比传值返回的函数效率高,在需要频繁或大量返回返回值的时候,就可以使用传引用返回
但是使用传引用返回时,有着许多的注意事项。
使用场景注意
现在有如上函数,其函数内部的n是定义在count2()内部的变量,返回值的类型是int&,此时我们对它进行调用
可以发现运行结果是我们所想要的结果,但如果我们在对它进行二次调用并在调用前再运行一个函数
此时我们会发现,虽然我们依然是调用的test,但是得到结果却已经被修改为了10,而修改值正好是count1()中定义的x的值。其原因在于函数调用过程。
那我们都知道,函数在调用的时候会在内存空间的栈中申请一块空间,叫做建立栈帧。而函数中的各项数据就存储在栈帧中。内存空间中还有叫堆的空间,动态申请的空间就来自于这里。另外还有静态区和常量区两个空间,分别存储静态变量和常量。下图是一个粗略的示意图
而栈是向下生长的,意味着先建立的栈帧在上面,后建立的栈帧在下面。
同时,函数结束后建立的栈帧就会被销毁。但这里的销毁并不是真正的将空间销毁,而是将调用的空间返回给栈。
打个比方,就和租房一样,租客是函数,从房东栈那里租了一个房间,得到了该房间的使用权,即建立了一个栈帧。当租客准备离开,不再租房时,就会将房间还给房东,这就是销毁空间。但是租客退房后,该房间依然存在于房东手里,并在未来会租给其他租客。函数的调用与销毁就是这个道理,销毁后的空间并不是凭空消失了,而是返还到了栈的手里,在后面有其他函数调用时就会再次使用这片空间。
那么这样我们就明确了为什么test的值在调用另一个函数时会被修改了。因为test所获得的空间在count1()结束时就被返还给了栈空间,此时这片空间的数据不再被保护,就像当你租房时房东在力所能及的返回内保护你的个人财产,但当你离开后就不再保护。而函数内的n就像是你遗留在房间内的物品,当你退房后再次用其他手段进入该房间,你所遗留的物品可能还在,也可能你的物品所存放的位置被其他租客的物品所替代了。因此,尽管test依然有片空间的地址,但再次调用时该空间内的数据可能就被修改了
但如果我们函数内的是用static修饰的静态变量,那么结果又会不同
在这里test的值就没有被修改。因为静态变量并没有存在栈帧中,而是存在另一个叫做静态区的空间,当栈帧被销毁时,并不会影响到静态区的变量。
现在引用返回值可能被修改的原因我们知道了,那为什么传值返回不会被修改呢?因为传值返回并不是单纯的返回调用函数内n的值,而是返回的n的拷贝值。如下所示:
n在返回时将它的值传给了临时变量p,然后n随着栈帧的销毁也跟着销毁,此时p再将它所存储的值传给了test,然后p再销毁。这也是为什么引用返回不会申请内存空间而传值返回需要申请内存空间。可以看成引用返回返回的是对应的空间地址上的值,而传值返回则是需要申请一块空间建立一个临时变量来返回。
2.修改返回值
在前文中就提到了,引用可以修改原变量的值,依靠这个特性,传引用返回的另一个作用就是修改返回值,多用于数据结构内。
以顺序表为例,假如要将顺序表中的所有2的倍数的数全部变成2倍,那么就可以直接用以引用为返回值的函数SLAt(),而无需再定义一个指针变量来接收该值并进行修改。无论是效率还是便捷程度都比指针高
size_t SLSize(SL* sl)
{
assert(sl);
return sl->size;
}
SLDataType& SLAt(SL* sl, int pos)
{
assert(sl);
assert(pos);
return sl->data[pos];
}
void test(SL* sl)
{
assert(sl);
for (int i = 0; i < SLSize(&sl) ++i)
{
if (SLAt(&sl, i) / 2 == 0)
{
SLAt(&sl, i) *= 2;
}
}
}
(2)做参数
引用做参数同样有两个作用,一个是减少拷贝 ,提高效率。另一个是做输出型参数,使得函数中修改形参可以修改实参。
1.减少拷贝,提高效率
在引用做返回值中也说过了,传值返回并不是返回其变量本身的值,而是返回其在内存空间中拷贝的值。同理,做参数传参时也并不是传的实参本身的值,而是在内存中对实参的拷贝,再将拷贝的值赋予函数中的形参。由此,引用做参数也是直接拿到实参的值,无需中间的拷贝过程。进而达到减少拷贝,提高效率的作用
2.引用做参数
引用在函数内部其实并没有什么意义,因为仅仅只是给一个变量取了一个别名,当原变量改变时引用的值也会改变,而当引用改变时,原变量也会改变。他们之间是互相影响。
但是,也正因为可以相互影响且使用起来比较简洁明了,函数中做参数是引用的一大引用场景。
void swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
int main(void)
{
int a = 10;
int b = 20;
swap(a, b);
return 0;
}
不用像指针那样需要解引用和传地址,使用起来非常的方便。但是在使用时也需要有许多细节需要注意,其中最值得注意的就是对const修饰的变量的引用
我们都知道,const修饰的变量无法修改,因此我们可以将其看做具有“常性”,即具有常数的性质。
那么问题就来了,引用是一个变量的别名,可以用引用来修改原变量的值。当然我们也知道无法将常数进行引用,但如果此时这个变量是由const修饰的变量,那么引用能否成功呢?答案当然是否定的。前文也说了const修饰的变量无法修改,具有“常性”,一个无法修改的值当然是无法进行引用的,因此,由const修饰的变量无法进行引用
但是如果我们用由const修饰的引用对一个普通变量或const修饰的变量进行引用能否成功呢?答案是肯定的。
原因在于变量的权限问题。引用一个用const修饰的变量,此时原变量的权限使其自身不能改变,但引用的权限却能够修改原变量,导致引用权限过大,进而引用失败。
但用由const修饰的引用去引用一个普通变量时,原变量的权限是可以修改,而引用的权限使引用变量无法修改原变量,引用权限缩小,引用成功。而引用const修饰的变量时则属于权限平级,双发都无法修改值,引用成功。
由const限定符可以延伸到函数传参中。const限定符也可以修饰函数中的参数,由此围绕着const的使用出现了两种参数引用形式。
(1)函数中加上const限定符
void test(const int& a)
{
cout << a << endl;
}
那我们现在都知道了由const修饰的引用既可以引用普通变量,也可以引用由const修饰的变量。因此,当我们的函数传入的引用不需要对实参进行修改时,就要将参数中的引用加上const,这样当函数调用传参时就既可以传入普通变量,也可以传入由const修饰的变量,不会出现想传入的变量因为有const限定符而无法传入的情况
(2)函数中不加上const限定符
此形式一般用于引用做输出型参数的情况。当函数中的引用需要能够对实参产生影响的时候,比如写一个交换函数,就不能加const限定符,避免修改失败。