9.14 c++基础
目录
域的概念:
命名空间域封装函数和结构体
命名空间的嵌套:
std,c++官方库定义的命名空间
using namespace std
缺省参数:
全缺省参数:
半缺省参数:
函数重载:
为什么cout能够识别类型:
如何辨别出函数重载:
为什么c语言不支持函数重载,而c++支持函数重载?
返回值类型不相同是否构成函数重载:
引用:
引用的作用:
域的概念:
我们在c语言就接触过域,参数的作用域也就是域。
我们之前学习的域分为两部分,局部域和全局域。
这里,我们思考一个问题:当局部域和全局域同时存在时,我们该如何处理呢?
我们写代码进行尝试:
#include<stdio.h>
int a = 0;
int main()
{
int a = 1;
printf("%d", a);
}
如图所示的代码中,我们打印的结果是多少?
我们打印的结果为1,证明了当全局域和局部域同时存在时,我们要优先考虑局部域。
那我们有没有一种方法可以访问全局域中的参数呢?
如代码所示:
#include<stdio.h>
int a = 0;
int main()
{
int a = 1;
printf("%d\n", a);
printf("%d\n", ::a);
}
这里的::叫做域作用限定符,当域作用限定符之前没有具体的域的时候,我们访问的就是全局域。
如图所示,当我们使用域作用限定符时,我们打印的结果就是全局域中变量a的值。
我们思考一个问题:
在同一个域中,能不能有两个同名的参数变量?
#include<stdio.h>
#include<stdlib.h>
int rand = 10;
int main()
{
printf("%d", rand);
}
不能,这是上节课我们写的代码,我们进行编译:
错误的原因是重定义:
原因是这样:当我们运行代码时的预处理阶段,我们就会把头文件包含的内容进行展开,在头文件#include<stdlib.h>中,我们已经包含了一个叫做产生随机数的函数:
而我们在全局域中又创建了一个整型参数rand,这两个就会放生冲突。
所以:在同一个域中不能有两个同名的参数。
那假设我们要访问的是头文件中的rand,我们该如何操作:
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
}
int main()
{
printf("%d", rand);
}
namespace bit表示命名空间域,表示我们再创建一个域,这个域在正常情况下不会被直接访问,我们把我们的整型参数rand放在bit域中,我们再进行编译:
这时候,我们打印的就是预处理阶段,头文件展开中的rand函数,因为打印函数时,默认打印的是函数的地址。
我们也可以用地址的16进位制显示一下:
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
}
int main()
{
printf("%p", rand);
}
这时候,我们再思考一个问题:在该命名空间域定义的变量是属于全局变量还是局部变量。
我们可以这样分析:首先,我们常使用的有三个空间,三个空间分别是:栈,静态区,堆
其中:栈帧是在函数调用时产生的,静态区是用来存储全局变量的,堆是动态申请开辟的空间产生的。
因为局部变量全部是存储在函数中的,所以局部变量也是在栈帧的范畴中,而rand并不是在函数中的,所以不是局部变量,而是全局变量。
总结:命名空间域不影响变量的生命周期,只是限定域,编译查找规则。
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
}
void func()
{
printf("%d\n", rand);
}
int main()
{
func();
printf("%d", rand);
}
我们打印的结果是多少?
原因是我们在查找是时,默认现在局部域中找,没有找到rand,再去全局域中找,命名空间域限定了域,所以我们只能在当预处理时,头文件展开时才能访问到#include<stdlib.h>,我们访问的是rand函数,打印的是rand函数的地址。
我们可以使用域操作限定符来访问域bit中的rand
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
}
void func()
{
printf("%d\n", bit::rand);
}
int main()
{
func();
printf("%d", bit::rand);
}
我们的默认查找规则是先在局部找,再在全局找。
但是当我们使用域操作限定符时,我们默认会在域bit中找。
命名空间域封装函数和结构体:
namespace bit
{
int rand = 10;
int x = 1;
int Add(int left, int rigt)
{
return left + right;
}
struct Node
{
struct Node*next;
int val;
};
}
我们思考:用命名空间域封装函数Add和结构体Node和不加命名空间有什么区别。
答:区别在于当用命名空间封装时,当我们没有使用域操作限定符时,我们是不能访问对应的函数和结构体的。
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
int x = 1;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node*next;
int val;
};
}
int main()
{
printf("%d\n", Add(1, 2));
return 0;
}
当我们进行运行时:
我们找不到函数Add的话,就必须得使用域操作限定符:
int main()
{
printf("%d\n", bit::Add(1, 2));
return 0;
}
进行编译:
当我们需要用结构体时,我们需要把域操作限定符放到struct的后面:
int main()
{
printf("%d\n", bit::Add(1, 2));
struct bit::Node node;
return 0;
}
为什么要把函数和结构体封装呢?因为我们可以使用一种更方便的写法:
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
int x = 1;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node*next;
int val;
};
}
namespace byte
{
int Add(int left, int right)
{
return left * 10 + right * 10;
}
struct Node
{
struct Node*next;
struct Node*prev;
int val;
};
}
这里我们的bit是一个域,我们的byte也是一个域,我们把byte域中的Add函数和结构体进行修改,并使两个域中函数和结构体的名字是相同的,假如在c语言中,我们是无法进行区分的,但是我们使用了两个域进行封装时,我们是可以进行区分的:
int main()
{
printf("%d\n", bit::Add(1, 2));
printf("%d\n", byte::Add(1, 2));
struct bit::Node node;
struct byte::Node node0;
return 0;
}
我们进行编译:
用命名空间域去封装函数很好的避免了命名冲突。
命名空间的嵌套:
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;
}
}
}
命名空间域N1嵌套着N2,要注意,我们使用N1和N2时,对应的操作限定符是不同的。
int main()
{
N1::a = 1;
N1::N2::c = 1;
}
我们如何找到c?
我们要现在域N1中找N2,找不到N2就报错,再在N2中找c,找不到c就报错。
std,c++官方库定义的命名空间
我们再添加两个头文件,一个头文件我们简写一个栈,一个我们简写一个队列:
我们在两个空间中使用了相同的命名空间,这两个命名空间是可以合并的。
#include<stdlib.h>
#include"Stack.h"
#include"Queue.h"
int main()
{
bit::StackPush(NULL, 1);
bit::QueuePush(NULL, 1);
return 0;
}
合并之后,我们都可以通过域操作限定符来使用命名空间内的函数。
总结:同一个工程中允许有多个相同名称的命名空间,编译器最后会合成同一个命名空间。
那么,同一个文件相同名称的命名空间可以被合并吗?
答:可以,看我们之前写的这串代码:
这两个头文件在预处理时会展开,展开会有两个相同名称的命名空间,这里就发生了合并,这里其实就等价于同一个文件相同名称的命名空间的合并。
using namespace std
我们在打印函数时,会遇到这种情况:
#include<iostream>
int main()
{
std::cout << "hello bit" << std::endl;
std::cout << "hello bit" << std::endl;
std::cout << "hello bit" << std::endl;
}
假如我们要多次打印时,我们每一次打印都需要写std::count std::endl,有没有什么写法可以避免这种问题?
我们可以使用using namespace std
using namespcae std的意思是展开命名空间std的内容,表示我们可以使用std官方库的函数参数等等。
#include<iostream>
using namespace std;
int main()
{
cout << "hello bit" << endl;
cout << "hello bit" << endl;
cout << "hello bit" << endl;
}
我们进行编译:
但是这里其实就存在一些隐患了,因为我们把std库都展开了,std库是非常庞大的,难免我们会定义出和std库中同名的参数或函数:
例如:
#include<iostream>
using namespace std;
int cout = 10;
int main()
{
cout << "hello bit" << endl;
cout << "hello bit" << endl;
cout << "hello bit" << endl;
}
我们进行编译:
错误的原因是我们定义的cout与std库中的cout产生了冲突。
我们可以采取封装的办法来处理这些问题:
#include<iostream>
using namespace std;
int cout = 10;
int main()
{
std::cout << "hello bit" << endl;
std::cout << "hello bit" << endl;
std::cout << "hello bit" << endl;
}
这样就表示我们要找的是std库中的cout。
我们进行编译:
当然,我们还可以这样写:
#include<iostream>
using std::cout;
int main()
{
cout << "hello bit" << std::endl;
cout << "hello bit" << std::endl;
cout << "hello bit" << std::endl;
}
我们进行编译:
cout << "hello bit" << std::endl;
我们对这串代码进行分析:
cout表示的是console out,意思就是控制台输出,就和c语言的printf类似
cin就是console in,表示的是控制台输入,和c语言的scanf函数类似。
endl就相当与"\n"的意思,就是换行。
这里的<<表示的是流插入运算符
cin使用的>>表示的是流提取运算符。
这里的cin和cout相较于c语言中的scanf和printf有能够自动识别类型的优点:
#include<iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cin >> a;
cin >> b >> c;
cout << a << endl;
cout << b << c << endl;
return 0;
}
我们进行编译:
我们输入a为3,b为3.33,c为a
我们可以发现,cin和cout可以自动识别类型。
缺省参数:
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func();
}
如图所示,我们看Func函数,我们可以发现Func函数的参数是int a=0,这是默认给的值。
缺省参数的意思:
1:假如我们调用Func函数没有传递参数,那么我们的参数就默认是为这里设置的0
2:当我们调用Func函数传递了参数,那我们的参数就为这个参数。
如图所示代码,我们的Func函数表示打印参数a,当我们不传参数时,我们的参数就默认为a,打印的结果就为0.
当我们传递参数时,如图所示:
#include<iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func();
Func(10);
}
我们打印的结果应该为10.
全缺省参数:
#include<iostream>
using namespace std;
void Func(int a = 0, int b = 10,int c=100)
{
cout <<"a=" <<a<< endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
如图所示,我们所有的参数全部都是缺省的,这时候,我们调用函数不传参时:
#include<iostream>
using namespace std;
void Func(int a = 0, int b = 10,int c=100)
{
cout <<"a=" <<a<< endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
int main()
{
Func();
}
我们调用的结果应该就是:0,10,100.
我们传递参数时,需要从左向右传,并且不能有中断,例如,我们可以传参数a,b
#include<iostream>
using namespace std;
void Func(int a = 0, int b = 10,int c=100)
{
cout <<"a=" <<a<< endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
int main()
{
Func(10,20);
}
这样,c就应该是默认值100,a和b就应该是参数值10和20.
但是我们不能传递参数b,c或a,c
注意:我们传递的第一个参数对应的就是函数的第一个参数,第二个参数对应的就是函数的第二个参数:
半缺省参数:
#include<iostream>
using namespace std;
void Func(int a, int b = 10,int c=100)
{
cout <<"a=" <<a<< endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
如图所示,参数a没有缺省,其他参数缺省了,这种就叫做半缺省参数。
注意:半缺省参数必须保证参数从右往左是连续的:
例如:这样的半缺省就是错误的:
或者是这样:
总结:缺省参数的参数列表必须是从右到左连续的,不能有中断。
调用缺省参数的函数的参数必须是从左到右连续的,不能有中断。
缺省参数不能声明和定义同时出现:
例如:
缺省参数不能声明和定义同时出现就是为了防止这种情况,也就是缺省参数的声明和定义并不一致的问题,所以我们不能在定义中使用缺省参数,只能在声明中使用缺省参数。
函数重载:
重载的意思就是“一词多义”
我们写代码进行演示:
#include<iostream>
using namespace std;
int Add(int x, int y)
{
cout << x + y << endl;
return x+ y;
}
double Add(double x, double y)
{
cout << x + y << endl;
return x + y;
}
这两个就是函数重载。
函数重载:函数名相同,参数不同(个数,类型,顺序)。
int Add(int x, int y)
{
cout << x + y << endl;
return x+ y;
}
double Add(double x, double y)
{
cout << x + y << endl;
return x + y;
}
int main()
{
int a = 10;
int b = 12;
Add(a, b);
double x = 2.22;
double y = 3.33;
Add(x, y);
}
这两个函数本质上是两个不同的函数,但是他们的函数名却相同。
函数重载在交换函数中的使用:
void Swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(char*a, char*b)
{
char tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 1, b = 1;
int c = 2.2, d = 3.3;
Swap(&a, &b);
Swap(&c, &d);
}
为什么cout能够识别类型:
cout << a;
cout << c;
本质上是cout在接收a的时候,调用了函数,这个函数其实也是函数重载,例如我们在接收a和接收c的时候,调用了不同的函数:
cout << a;
//ostream& operator<<(int val)
cout << c;
//ostream& operator<<(double val)
这些不同的函数用来接收我们传递来的不同的参数,进而实现自动识别类型的形式。
如何辨别出函数重载:
1:参数个数不同:
void f()
{
cout << "f()"<<endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(1);
return 0;
}
我们进行编译:
2:参数的类型不同:
void Swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(char*a, char*b)
{
char tmp = *a;
*a = *b;
*b = tmp;
}
例如我们的Swap函数就是类型不同。
3:参数的顺序不同
参数的顺序不同的本质其实就是类型不同:
例如:
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(1, 'a');
f('a', 1);
return 0;
}
我们尝试把缺省参数和函数重载结合起来:
void f()
{
cout << "f()" << endl;
}
void f(int a = 0, char c = 'a')
{
cout << "f(int a=0,char c='a')" << endl;
}
int main()
{
return 0;
}
如图所示:上面的两个函数是函数重载吗?
答:是,因为他们的函数名相同,参数类型不同。
我们进行编译:
但是当我们进行调用时:
void f()
{
cout << "f()" << endl;
}
void f(int a = 0, char c = 'a')
{
cout << "f(int a=0,char c='a')" << endl;
}
int main()
{
f();
return 0;
}
我们进行编译:
错误的原因是这样:因为有缺省参数的存在,所以我们无法区分我们调用的是函数f()还是f(int a=0,char c='a')。造成二义性。
所以,我们不要同时使用缺省参数和函数重载。
为什么c语言不支持函数重载,而c++支持函数重载?
答:我们之前学习的时候,学到过符号表,符号表中保存的是函数名及其地址,在c语言中,符号表中是这样写的:
在c++中,对于函数重载,符号表中是这样写的。
在c++中,对应的函数名是由函数名修饰规则决定的。
函数名修饰规则的主题就是:参数不同,其函数对应的函数名就不同。
既然函数名不同,那么对应的地址就不同。
返回值类型不相同是否构成函数重载:
int f(int a, int b)
{
cout << "int f(int a,int b)" << endl;
return 0;
}
char f(int a, int b)
{
cout << "char f(int a,int b)" << endl;
return 'a';
}
我们逐步进行分析:因为函数命名规则是根据函数的参数决定的,因为他们的函数的参数是相同的,所以不是函数重载。
那么,假如我们把函数的返回值类型也作为函数命名规则的一部分,那我们是否构成函数重载?
答:不构成,我们可以这样理解,返回值是不能用来区分函数的,例如,当我们在这里调用f(a,b)和f(a,b),我们是不清楚具体调用的是哪个函数的,所以产生歧义,导致二义性。
引用:
int a = 10;
int &ra = a;
引用就是取别名的意思,所以这里对应的图像就是这样:
ra和a对应的地址是相同的,换言之:ra就是a,a就是ra,相当于拷贝了一个a。
所以,对a的处理也会对ra造成影响:
引用的作用:
我们举一个例子:
void Swap(int *a, int *b)
{
int m = *b;
*a = *b;
*a = m;
}
我们从前写Swap函数就是这样写的,因为我们的形参的改变并不影响实参,所以我们要使用指针的方法。
但是现在我们可以这样写。
void Swap(int&a, int &b)
{
int m = b;
b = a;
a = m;
}
因为我们的形参就是实参的引用,换而言之,形参和实参指向的是同一个位置,所以我们修改形参也会使实参发生改变。
void Swap(int&a, int &b)
{
int m = b;
b = a;
a = m;
}
int main()
{
int c = 10;
int d = 20;
Swap(c, d);
}
如图所示:
我们从前写的单链表:
typedef struct ListNode
{
struct ListNode*next;
int val;
};
void SlistPushBack(struct ListNode**phead, int x)
{}
单链表进行尾插时,我们要传结构体的二级指针,因为我们要修改1级指针的值。
那我们现在就可以这样写了:
typedef struct ListNode
{
struct ListNode*next;
int val;
};
void SlistPushBack(struct ListNode*&phead, int x)
{}