屏幕前的你,一起加油啊!!!
一、命名空间(namespace)
1.命名空间的定义(::域作用限定符)
a. 之前的C语言学习中我们就了解过全局和局部这部分的知识了,在C++里面他们有一个新的名词就是域,域就相当于一片领地,如果想定义两个一模一样的变量在同一个域中,这显然是不行的,会出现变量重命名的问题,但是这样的问题还是比较常见的,因为c++和C语言中都有很多的模板,函数库等等,难免我们定义的和库里面定义的,产生命名冲突和名字污染,namespace所创建的命名空间就是用来解决这样的问题的。
为了防止命名冲突的产生,C++规定了命名空间的出现,这可以很好的解决问题,我们可以把我们想定义的东西都放到我们自己定义的命名空间里面去,这样就不会产生命名冲突的问题了。
#include <stdio.h> #include <stdlib.h> int rand = 10; // C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决 int main() { printf("%d\n", rand);//stdlib.h文件里面有rand函数。 return 0; } // 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“rand函数”
b. 编译器有一套自己的查找变量的规则:
1.默认先去局部域找变量,再到全局域里面去找变量
2.如果我们利用域作用限定符限定了编译器要查找的域,那编译器就会按照我们设定的查找规则来查找
#include <stdio.h> int a = 0;//全局域中定义变量a int main() { int a = 1;//局部域中定义变量a,所以在不同的域中是可以定义同一个变量的。 printf("%d\n", a);//先去局部域里面找a,再到全局域里面找a // ::域作用限定符 printf("%d\n", ::a);//::的左面是空白,代表作用于全局域,指定编译器查找的位置是全局域 return 0; }
b. 编译器有一套自己的查找变量的规则:
1.默认先去局部域找变量,再到全局域里面去找变量
2.如果我们利用域作用限定符限定了编译器要查找的域,那编译器就会按照我们设定的查找规则来查找
#include <stdio.h> int a = 0;//全局域中定义变量a int main() { int a = 1;//局部域中定义变量a,所以在不同的域中是可以定义同一个变量的。 printf("%d\n", a);//先去局部域里面找a,再到全局域里面找a // ::域作用限定符 printf("%d\n", ::a);//::的左面是空白,代表作用于全局域,指定编译器查找的位置是全局域 return 0; }
c. 我们现在利用命名空间wyn封装起来了rand,这时候就不会和stdlib.h文件中的rand()函数产生命名冲突了。定义的形式请看下面代码。
命名空间中的rand不是一个局部变量,而是一个全局变量,因为只有定义在函数里面,存放到栈上的变量才是局部变量。rand存放在静态区,并且现在的namespace根本就不是一个函数,自然也就说明rand不是局部变量,而是全局变量。
那么变量定义在命名空间中和定义在全局域中有什么区别呢?
其实区别就是编译器查找的规则不同,如果你指定查找的域,那编译器就去你定义的命名空间查找,如果你不指定查找的域,那编译器就先去局部域查找,再去全局域查找。
d. 命名空间也可嵌套定义,一个命名空间当中又细分多个命名空间,这样也是可以的,下面代码的wyn空间当中就嵌套定义了N1,N1中又嵌套定义了N2。
namespace wyn { int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; namespace N1 { int a; int b; int Add(int left, int right) { return left + right; } namespace N2 { int c; int d; int Sub(int left, int right) { return left - right; } } } }
e.
同一个工程中的不同的文件允许存在多个相同名称的命名空间,编译器最后会合成到同一个命名空间当中去。
同一个文件里面的相同名称的命名空间也是会被编译器合并的。
2.命名空间的使用(三种使用方式)
C++官方封装好了一个命名空间叫做std,它和其他的一些命名空间都被封装到iostream头文件里面,C++所使用的cin和cout都被封装在iostream文件中的std命名空间。
这其实变相的帮助我们解决了一个问题,就是如果我们平常中的命名和官方库产生冲突时,我们也不害怕,因为两者所处的域是不同的,互不干扰。
a.利用域作用限定符
这种命名空间的使用方式堪称经典,使用语法:域名+域作用限定符+域中的成员名
b.展开整个命名空间
这种使用方式不是很推荐,因为一旦将命名空间全部展开,虽然我们在使用上可以直接使用,但是这会产生极大的命名冲突安全问题隐患,所以如果你只是写个小算法,小程序等等,可以这么使用。但如果在大型工程里面还是不要这么用了,因为出了问题,就麻烦了。
c.展开域中的部分成员
强烈推荐这样的使用方式,将我们常用的某些函数展开,我们在定义时,只要避免和部分展开的重名即可,这样的使用方式也较为安全,所以强烈推荐。
using namespace std;//这个东西存在的意义就是将命名空间里面的内容展开,用起来会方便很多 //当然反面的意义就是将命名空间的域拆开了,会产生命名冲突问题的隐患。 //日常练习,写个算法或小程序等等,这么使用可以,因为一般情况下不会产生命名冲突的问题,但项目里面最好不要这么用。 using std::cout;//将常用的展开,自己在定义的时候,尽量避免和常用的重名即可。 int main() { //下面的所有访问都必须在iostream文件展开的基础上进行,只有展开后,那些大量的命名空间才会出现,下面的代码才可以访问 //命名空间里面的变量、函数、结构体等等 //第一种访问方式:指定域访问 std::cout << "hello world" << std::endl; std::cout << "hello world" << std::endl; std::cout << "hello world" << std::endl; //第二种访问方式:将域在前面全部展开,编译器会先在局部找cout,然后去全局找cout,正好域全部展开了,全局自然存在cout cout << "hello world" << endl; cout << "hello world" << endl; cout << "hello world" << endl; //第三种访问方式:将域指定展开,只展开域中的某些常用成员,方便我们使用那些常用的函数或结构体。 cout << "hello world" << std::endl;//endl没有被展开,需要指定访问的域 cout << "hello world" << std::endl; cout << "hello world" << std::endl; }
二、C++输入&输出(iostream+std)
a.
使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
b.
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。
c.
<<是流插入运算符,>>是流提取运算符,endl是特殊的C++符号,表示换行输出,他也被包含在iostream头文件中
注意:
早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C语言的头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;所以我们可以看到iostream是不带.h的。
#include <iostream> using namespace std;//我们平常写的时候全部展开std命名空间,以后写项目还是尽量不要这样写。 int main() { //可以自动识别变量的类型,相比c的优势printf / scanf // << 流插入运算符 cout << "hello world!!!" << '\n'; cout << "hello world!!!" << endl;//两种写法等价,C++喜欢下面这种写法 int a; double b; char c; // >> 流提取运算符 cin >> a; cin >> b >> c; cout << a << endl; cout << b << ' ' << c << endl; //C++也有不方便的地方,如果你要输出小数点后几位,建议还是使用C语言来实现 //还有一种场景是要求挨着打印出变量的类型,这时候用C语言也是较方便的。 //C和C++哪个方便就用哪个 printf("%.2f\n", b); cout << "int:" << a << ' ' << "double:" << b << ' ' << "char:" << (int)c << endl;//c前面加个强制类型转换,输出ascll printf("int:%d double:%.2f char:%d", a, b, c); return 0; }
三、缺省参数
在声明或定义函数时,给函数指定一个缺省值。
调用该函数时,如果实参没有指定传什么,函数就使用该缺省值,如果指定了,那就使用实参的值。
#include <iostream> using namespace std; void Func(int a = 0)//缺省值 { cout << a << endl; } int main() { Func();//函数拥有缺省值之后,可以给函数传参也可以不给他传参。 Func(10); return 0; }
1.全缺省参数
在给全缺省参数的函数传参时,我们必须从左向右依次传参,不可以中间空出来,跳跃的传参,这样的传参方式编译器是不支持的
void Func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(); Func(1);//默认传给了a Func(1, 2);//从左向右依次传参数 //Func(1, , 2);//不可以中间空出来,跳过b,只给a和c传,这样编译器是不支持的。 Func(1, 2, 3); return 0; }
2.半缺省参数
半缺省参数的函数在设计时,缺省参数必须得是从右向左连续缺省。
void Func(int a, int b = 10, int c )//这样是不可以的 { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } void Func(int a, int b , int c=10)//这样是可以的 { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
3.(带有缺省参数)函数的定义和声明
a.
带有缺省参数的函数在定义和声明时,C++有特殊的规定,在函数的声明部分中写出缺省参数,在函数的定义部分中不写缺省参数,如下面代码所示。
b.
如果声明与定义中同时出现缺省值,而恰巧两个缺省值是不同的,这样的话,编译器是无法确定改用哪个缺省值。
c.
缺省值必须是常量(情况居多)或全局变量,C语言是不支持缺省参数这种概念的。
四、函数重载(一个名字多个函数)
1.函数重载的类型
C++允许在一个域中同时出现几个功能类似的同名函数,这些函数常用来处理实现功能类似数据类型不同的问题。
下面的两个函数在C++中是支持同时存在的,但在C语言中是不支持的。
void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } void Swap(double* p1, double* p2) { double tmp = *p1; *p1 = *p2; *p2 = tmp; }
1.1 形参的类型不同
int add(int x, int y)//int类型 { return x + y; } double add(double x, double y)//double类型 { return x + y; }
1.2 形参的个数不同
void f()//0个形参 { cout << "f()" << endl; } void f(int a)//一个形参 { cout << "f(int a)" << endl; } int main() { f(); f(1); return 0; }
1.3 形参的类型顺序不同
void f(int a, char b)//int char { cout << "f(int a,char b)" << endl; } void f(char a, int b)//char int { cout << "f(char a,int b)" << endl; } //上面的两个函数算函数重载,因为参数的类型顺序不同 //下面的两个函数不算函数重载,因为编译器是按照参数类型识别的,并不是按照参数的名字来识别的。 void f(int a, int b)//int int { cout << "f(int a,int b)" << endl; } void f(int a, int b)//int int { cout << "f(int a,int b)" << endl; } int main() { f(106, 'A'); f('A', 106); return 0; }
2.函数重载+缺省参数(编译器懵逼的代码)
下面的两个函数确实构成函数重载,但在调用时,如果我们不指定实参的值,那就会产生二义性,编译器是不知道该调用哪个函数的,所以我们尽量不要写出这样的函数重载。
void f()//0个形参 { cout << "f()" << endl; } void f(int a = 0, char b = 1)//2个形参 { cout << "f(int a,char b)" << endl; } int main() { f(10); f(10, 20); f();//这里会报错:对重载函数的调用不明确 -- 产生二义性 -- 编译器蒙蔽 return 0; }
3.C++支持函数重载的原理(汇编下的函数名修饰规则)
稍微带大家复习一些程序运行原理部分的知识:
假设当前a.cpp中调用了b.cpp中定义的Add函数,那么在编译结束之后,a.cpp和b.cpp都会产生目标文件.obj,每个目标文件中都会有他们自己的符号表,汇总了全局域里面的函数名,变量名,结构体名等等。
编译器看到a.obj中调用了Add函数,但是没有Add的地址,这时链接器就会到b.obj中找到Add的地址并且把它链接到一起,进行符号表的合并。
那么在链接时遇到函数,编译器是依靠什么来找到函数的地址呢?依靠的其实就是函数名,每个函数名又都有自己的函数名修饰规则,我们接下来用gcc和g++编译器看一下汇编代码中的函数名是如何修饰的
_z3Addii,
3代表3个字符,紧跟着函数名Add,然后是参数类型的缩写ii分别是int int
_z4funcidpi,
4代表4个字符,紧跟着函数名func,然后是参数类型的缩写idpi分别是int double int*
所以C语言没办法支持重载,因为同名函数在底层汇编中是无法区分的。
而C++可以通过函数名修饰规则,来区分同名函数。只要参数(个数、类型、类型顺序)不同,汇编底层中修饰出来的函数名就不一样,也就支持了函数重载。
4.返回值不同能否构成函数重载?
函数在调用时指定的是参数的类型,并没有指定返回值的类型。
所以在调用函数时,编译器只是通过参数来确定到底要调用哪个函数。
两个函数如果只有返回值类型不同的话,编译器是无法区分到底要调用哪个函数的,这会产生二义性。
int f(int a, int b) { cout << "f(int a,int b)" << endl; return 0; } char f(int a, int b) { cout << "f(int a,int b)" << endl; return 'A'; } // 上面的两个函数不构成函数重载 int main() { f(1, 1); f(2, 2); return 0; }
五、引用(三姓家奴)
1.引用概念(不就取别名么)
引用不是新定义一个变量,而是给已存在变量取了一个别名。
语法层面上,编译器不会为引用变量开辟内存空间,它和引用实体共用同一块内存空间。
ra和a的地址是相同的,说明ra和a共用同一块内存空间。
void TestRef() { int a = 10; int& ra = a;//<====定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); } int main() { TestRef(); return 0; }
了解引用后在写链表时,就不需要传二级指针了,我们可以直接对一级指针进行引用,这样操作的时候引用参数也可以变成输出型参数。
void SlistPushBack(struct ListNode** pphead, int x)//以前C语言的用法 void SlistPushBack(struct ListNode*& phead, int x)//给int*取别名,其实就是给指针变量取别名 { //有了别名之后,完全不需要二级指针了。 } //有些教材会这样去写 typedef struct ListNode { struct ListNode* next; int val; }LTNode,*PLTNode; void SlistPushBack(PLTNode& phead, int x)//这里其实就是一个结构体指针的引用 { }
2.引用特性
a.引用在定义时必须初始化
b.一个变量可以有多个引用
c.一旦引用了某个实体,不可以在引用其他实体
void TestRef() { int a = 10; int& ra = a; int& rrra=a; int& rrrra=a;//变量a可以有多个引用 int& rra;//必须初始化引用,不能空引用 int b = 20; ra = b; //这里是赋值操作,不是修改引用,引用一旦引用一个实体,就不能再引用其他实体, //ra就是a,a就是ra,修改ra自然就是修改a了。 //C++里面引用无法完全替代指针,链表无法用引用实现,所以该用指针还得用指针。 //为什么实现不了捏?因为引用无法拿到下一个节点的地址呀! }
3.引用的使用场景
3.1 内存空间销毁意味着什么?& 访问销毁的内存空间会怎样?
内存空间销毁并不是把这块内存空间撕烂了,永久性的毁灭这块儿空间,内存空间是不会消失的,他会原封不动的在那里,只不过当内存空间销毁之后,他的使用权不属于我们了,我们存到里面的数据不被保护了,有可能发生改变了。
销毁之后,我们依然可以访问到这块空间,只是访问的行为是不确定的,我们对空间的数据进行读写的行为是无法预料的。
销毁的空间可能发生的改变:
a.这块空间没有分配给别人,我们写的数据并未被清理掉,依旧可以正常访问到原来的数据
b.这块空间没有分配给别人,但是空间数据已经被销毁了,呈现出随机值
c.这块空间分配给别人,别人写了其他的数据,将我们的数据覆盖掉了。
上面的人是我们拟人化了,实际上他就是某些变量或结构体或函数栈帧等等……
3.2 做返回值(减少拷贝提高效率,修改返回值)
1.减少拷贝,提高效率
当我们要返回一棵树的时候,引用返回就可以帮我们大忙了,由于它不用拷贝,所以相比于传值返回,程序在运行上,效率提升的可不止一点。
下面图片所得结论:出了函数作用域,返回变量存在,可以使用引用返回,不存在,不可以使用引用返回。
2.
能否用引用返回,取决于出了作用域,要返回的对象是否还存在,如果存在,则可以用引用返回,如果被销毁则不可以用引用返回。
寄存器其实也是需要拷贝的,先将局部变量的值拷贝到寄存器,然后再把寄存器的值拷到接收函数返回变量。
二、修改返回值
要知道,函数的返回值它是一个值,也就是一个临时变量,临时变量是具有常性的,是一个值,并不是一个变量。
所以如果不用引用返回的话,肯定是无法修改返回值的,编译器会报错:表达式必须是可修改的左值。
但是如果你用引用返回的话,我们就可以修改返回值了,因为引用变量是返回值的一个别名,首先引用变量就是这个返回值本身,并且引用还是一个变量,是可以修改的左值,所以我们可以利用引用做返回值来修改返回值,这一点在C语言中是无法做到的,因为C语言中返回值他只是一个值,并不是变量,无法修改,但C++有了引用之后便可做到这一点。
下面的两段代码给大家演示了C语言中,返回值无法修改的场景。
int* modify(int*pa) { int b = 10; pa = &b; return pa; } int change(int* arr) { for (int i = 0; i < 3; i++) { if (arr[i] == 2) { return arr[i]; } } } int main() { int a = 100; int arr[] = { 1,2,3 }; change(arr) *= 2;//报错,表达式必须是可修改的左值 modify(&a) *= 2;//报错,表达式必须是可修改的左值 }
下面这段代码给大家演示了C++中利用引用作为返回值来修改返回值的场景。
将数组中的偶数全部扩大二倍。
int& change(int* arr,int i) { return arr[i]; } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; for (int i = 0; i < 10; i++) { if (arr[i] % 2 == 0) { change(arr, i) *= 2; } } for (int i = 0; i < 10; i++) { cout << arr[i] << " "; } }