1.命名空间
1.1命名空间的定义
在C/C++编程中,会使用大量的变量、函数、类。因为大量使用,难免会造成命名冲突,那么使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染。C++中的namespace关键字就是解决这个问题的。
#include <stdio.h>
int printf;
int main()
{
return 0;
}
这个问题可以映射到我们参与的项目当中,别人写了一个超大的头文件,我们正好要使用头文件,在我们自己源文件中定义某些变量可能会与头文件中的变量发生命名冲突,这就会使得我们觉得这个报错莫名奇妙。
我们解决的方案是将变量装在不同的命名空间当中,这个命名空间在静态区中,但是它并不占用空间,它只是修饰了在此空间定义的变量,在我们使用要使用此命名空间的变量时,必须指明变量的命名空间。
#include <stdio.h>
namespace A
{
int printf = 10;
}
int main()
{
printf("%d", A::printf);
return 0;
}
举一个形象化的例子来理解命名空间:我们有一块草地,有一只羊、一匹马和一头牛,某天我们引进了一只与原有一摸一样的羊,为了区分,我们把引进的那只羊圈养起来,就可以方便以后的处理。命名空间便是把变量、函数、类型等“圈养”起来,方便与其他域的变量、函数、类型等区分。
在C语言中存在一个弊端,在C++中能够被解决。即当我们定义了两个相同的全局变量和局部变量,在函数中使用某一变量的时候,都只能对局部变量进行操作。而C++命名空间的出现便能解决这个问题。
#include <stdio.h>
int x = 10;
int main()
{
int x = 30;
printf("%d\n", x);
printf("%d\n", ::x);
return 0;
}
命名空间的玩法没有上述的那么单一,我们可以在命名空间中嵌套定义命名空间、定义函数、定义类型等操作。
namespace B
{
int x = 30;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
struct Stu
{
char name[20];
int age;
};
namespace C
{
int x = 10;
}
}
并且当我们定义了两个相同的命名空间时,编译器会自动将他们合并。
#include <stdio.h>
namespace B
{
int x = 30;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
struct Stu
{
char name[20];
int age;
};
namespace C
{
int x = 10;
}
}
namespace B
{
int y = 30;
}
int main()
{
printf("%d %d\n", B::x, B::y);
return 0;
}
这点体现在官方库的命名空间std中。因为C++提供给我们的头文件很多,每个头文件如果都使用不同的命名空间,将会增加我们的代码量。C++的做法便是统一使用std这个命名空间,在编译的时候会把不同头文件的同名命名空间合并。
总结: 1.命名空间可以定义变量、函数、类型等 2.命名空间可以嵌套定义 3.同名的命名空间在编译时会自动合并 4. 一个命名空间的成功定义就证明增加了一个新的作用域,所有在命名空间中的变量、函数等不能被外部使用。若要使用命名空间中的变量等,就必须加使用"::"作用域限定符限定命名空间。
1.2命名空间的使用
刚才我们提到了C++官方库的命名空间std,现在我们使用它来输出一个"hello world"。
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
using namespace std; 的作用是,从这里开始,往后的std命名空间中的变量、函数、类型等不需要使用作用域限定符限定。cout和endl都是std命名空间中的变量,因为放开了std命名空间,所以不需要用作用域限定符限定。
这种做法是全部放开,可能会导致出现命名冲突的问题。我们还可以使用局部放开来使用命名空间中的变量。
#include <iostream>
using std::cout;
using std::endl;
int main()
{
cout << "hello world" << endl;
return 0;
}
第三种方式便是使用作用域限定符。
#include <iostream>
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
总结: 命名空间的使用有三种方式。 1.全部放开 2.局部放开 3.不放开,使用作用域限定符
2.缺省参数
2.1缺省参数概念与分类
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include <iostream>
using namespace std;
void test(int x = 10)
{
cout << x << endl;
}
int main()
{
test();
test(80);
return 0;
}
可以看到打印结果,当我们没有指定实参时,test函数会将形参初始化为10;而当我们指定实参时,test函数的形参便是我们实参的值。
当我们的函数有多个形参时,我们可以让它的形参全部缺省,也可以部分缺省。但部分缺省参数必须是从右往左连续;实参的传递必须是从左往右连续;并且缺省值必须为常量或全局变量。
#include <iostream>
using namespace std;
void test1(int x = 10, int y = 20, int z = 30)
{
cout << x << " " << y << " " << z << endl;
}
void test2(int x, int y = 20, int z = 30)
{
cout << x << " " << y << " " << z << endl;
}
void test3(int x = 10, int y, int z = 30)
{
cout << x << " " << y << " " << z << endl;
}
int main()
{
test1();
test1(100, 200);
test2();
test2(100);
return 0;
}
总结: 1.缺省参数是声明或定义函数时为函数的参数指定一个缺省值 2.部分缺省参数必须是从右往左连续;实参的传递必须是从左往右连续; 3.缺省值必须为常量或全局变量
2.2缺省参数的使用
缺省参数的实际应用在于我们的项目当中。例如在我们使用C语言实现通讯录当中的初始化函数。函数自动将通讯录的人数初始化为0,在以后的添加联系人的过程当中存储空间呈2倍增长,这无疑是空间的浪费。例如当我们确定我们的通讯录有100个人,而在存储的过程当中空间会被开到能够存储128人的空间。所以缺省参数就能解决这一空间浪费的问题。
void ContactInit(int size = 0)
{
;
}
int main()
{
ContactInit();
ContactInit(100);
return 0;
}
值得注意的是,在多文件操作中,头文件中有函数的声明,源文件有函数的定义。在给定缺省值时要避免声明与定义同时有缺省值的情况。
3.函数重载
3.1函数重载的概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
void Print(int x, int y, int z);
void Print(char x, char y, char z);
void Print(double x, double y);
void Print(int x, char y, double z);
void Print(char y, int x, double z);
void Pirnt(double z, int x, char y);
int main()
{
return 0;
}
函数重载的实质是形参的类型不同。如果某一函数的两个形参的类型都是相同的,在另一个同名的函数中调换它们两个的顺序是不构成重载的。
void Swap(int* p1, int* p2)
{
;
}
void Swap(int* p2, int* p1)
{
;
}
总结: 1.同名的函数参数类型不同、个数不同、顺序不同(不同类型)构成函数重载 2.同类型的形参不同顺序不构成重载
3.2函数重载的使用
在C语言中,每个函数的命名只能出现一次,这就会导致每次调用功能一样的不同函数时,调用的函数名总是不统一。函数重载的出现就优化了这一缺点,使得我们的代码更加简洁明了。
#include <iostream>
using namespace std;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Swap(char* p1, char* p2)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Swap(double* p1, double* p2)
{
double tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main()
{
int a = 1, b = 2;
Swap(&a, &b);
char x = 'w', y = 'v';
Swap(&x, &y);
double n = 1.123, m = 2.345;
Swap(&n, &m);
return 0;
}
3.3函数重载与缺省函数
当我们碰到一些特殊情况时,会使程序编译报错。
#include <iostream>
using namespace std;
void Print(int x = 10)
{
cout << x << endl;
}
void Print()
{
cout << "hello world" << endl;
}
int main()
{
Print(20);
Print();
return 0;
}
3.4C++为什么支持函数重载
在C/C++程序编译链接的过程中,会生成符号表,不同的的编译器会生成不同的符号表。在链接的过程中去寻找符号表对应的函数。 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。同时也确定了返回值的类型不同也不能构成重载,因为二者的编译器都没有对返回值进行修饰。
总结: 1.C与C++的编译器对函数修饰的规则不同 2.C没有办法区分同名的函数,所以不支持重载 3.C++可以区分同名的函数,构成函数重载
4.引用
4.1引用的概念
我们先关注一下引用的实例:
#include <iostream>
using namespace std;
int main()
{
int x = 0;
int& rx = x;
rx = 3;
cout << x << endl;
return 0;
}
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
例如三国里面的关羽,我们可以称他关羽,也可以称他关云长,也可以称他为武圣。云长是关羽的字,武圣是我们对他的尊称,二者都可以称为关羽的别名,通过别名就能识别出实体,也就是说,别名只是我们对实体的不同叫法。就像上面那段程序一样,我们定义了一个变量x,而后对它进行取别名,取为rx,我们对rx赋值,也就是对x赋值。 和指针一样,我们对某一个实体取别名时要注意引用的类型和实体的类型统一。
总结: 1.引用是对已存在的变量取别名 2.引用不占用内存空间 3.引用类型要与实体类型统一
4.2引用的特性
对应任何一个已存在的变量,我们可以给他取多个别名。即一个变量可以被多次引用。
int main()
{
int n = 0;
int& rn = n;
int& rrn = n;
int& rrrn = rrn;
return 0;
}
在定义引用时,必须被初始化。
int main()
{
int x = 0;
int& rx = x;
int& rrx;
return 0;
}
当一个引用引用了一个实体,这个引用便不能再引用其他实体。
4.3常引用
我们知道,const修饰变量时,会将这个变量赋予常属性,进而改变这个变量的类型。但是const也是一种权限修饰,即将可变的变量修饰成为了不可变的变量,即在const修饰之后,变量的权限变小了。那么在引用时,权限只能被缩小,不能被放大。
int main()
{
const int x = 3;
int& rx = x;
const int& rrx = x;
int y = 6;
int& ry = y;
const int& rry = y;
int& rrry = rry;
return 0;
}
4.4引用作函数参数
引用可以作为函数的参数,在某种程度上能够简化一些代码。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 3;
int m = 4;
Swap(n, m);
return 0;
}
引用也是可以构成函数重载的。
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 add(const int& x, int& y)
{
return x + y;
}
int main()
{
int a = 10;
const int b = 30;
add(a, b);
return 0;
}
4.5引用作返回值
我们回过头来思考这么一个问题:
int func()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = func();
return 0;
}
在函数栈帧中,当维护函数的两个寄存器指针不再维护这个栈帧时,函数内定义的临时变量便会销毁。那么返回值将被放在寄存器当中。
但是刚才我们说了,引用不占用内存空间,引用作返回值寄存器是不工作的,那么就会出现下面这个程序的问题:
#include <iostream>
using namespace std;
int& func()
{
int n = 0;
n++;
int& rn = n;
return rn;
}
int& test()
{
int n = 3;
n++;
int& rn = n;
return rn;
}
int main()
{
int& ret = func();
cout << ret << endl;
cout << ret << endl;
ret = test();
cout << ret << endl;
return 0;
}
那么解决这个问题方法就是让函数中定义的局部变量不销毁。
int& func()
{
static int n = 0;
n++;
int& rn = n;
return rn;
}
4.6引用与类型转换
类型转换是常用的编程手段,那么关于一般变量的转换并不是把某一变量扭转成其他类型,而是将这个变量的值赋给一个临时变量,通过这个临时变量再赋给其他的变量。这个临时变量具有常属性。那么在类型转换的引用过程当中,需要注意类型的匹配问题。
int main()
{
double x = 3.14;
int y = x;
return 0;
}
int main()
{
double x = 2.55;
int& rx = x;
const int& rrx = x;
return 0;
}
4.7引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int x = 0;
int& rx = x;
rx = 3;
int* px = &x;
*px = 3;
return 0;
}
观察汇编代码,可以看到引用的底层逻辑和指针的底层逻辑的实现是一样的。 但这并不能认为引用能够代替指针。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
|