C++入门(1) —— 命名空间、缺省参数、函数重载、引用

C++入门(1) —— 命名空间、缺省参数、函数重载、引用,第1张

目录
  • 1.命名空间
    • 1.1命名空间的定义
    • 1.2命名空间的使用
  • 2.缺省参数
    • 2.1缺省参数概念与分类
    • 2.2缺省参数的使用
  • 3.函数重载
    • 3.1函数重载的概念
    • 3.2函数重载的使用
    • 3.3函数重载与缺省函数
    • 3.4C++为什么支持函数重载
  • 4.引用
    • 4.1引用的概念
    • 4.2引用的特性
    • 4.3常引用
    • 4.4引用作函数参数
    • 4.5引用作返回值
    • 4.6引用与类型转换
    • 4.7引用和指针的区别

1.命名空间 1.1命名空间的定义

在C/C++编程中,会使用大量的变量、函数、类。因为大量使用,难免会造成命名冲突,那么使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染。C++中的namespace关键字就是解决这个问题的。

#include 

int printf;//假设我们不知道头文件中有printf这个函数
//此时就会编译报错

int main()
{
	return 0;
}


这个问题可以映射到我们参与的项目当中,别人写了一个超大的头文件,我们正好要使用头文件,在我们自己源文件中定义某些变量可能会与头文件中的变量发生命名冲突,这就会使得我们觉得这个报错莫名奇妙。

我们解决的方案是将变量装在不同的命名空间当中,这个命名空间在静态区中,但是它并不占用空间,它只是修饰了在此空间定义的变量,在我们使用要使用此命名空间的变量时,必须指明变量的命名空间。

#include 

namespace A//定义命名空间,空间的昵称是自定义的
{
	int printf = 10;//此时printf变量被命名空间A修饰,与头文件中的函数不冲突
}

int main()
{
	printf("%d", A::printf);//要使用A命名空间中的变量必须指明命名空间
	// :: 是一个作用域限定符
	return 0;
}

举一个形象化的例子来理解命名空间:我们有一块草地,有一只羊、一匹马和一头牛,某天我们引进了一只与原有一摸一样的羊,为了区分,我们把引进的那只羊圈养起来,就可以方便以后的处理。命名空间便是把变量、函数、类型等“圈养”起来,方便与其他域的变量、函数、类型等区分

在C语言中存在一个弊端,在C++中能够被解决。即当我们定义了两个相同的全局变量和局部变量,在函数中使用某一变量的时候,都只能对局部变量进行 *** 作。而C++命名空间的出现便能解决这个问题。

#include 

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 

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 
using namespace std;//放开std命名空间中的所有变量、函数、类型等

int main()
{
	cout << "hello world" << endl;
	return 0;
}

using namespace std; 的作用是,从这里开始,往后的std命名空间中的变量、函数、类型等不需要使用作用域限定符限定。cout和endl都是std命名空间中的变量,因为放开了std命名空间,所以不需要用作用域限定符限定。

这种做法是全部放开,可能会导致出现命名冲突的问题。我们还可以使用局部放开来使用命名空间中的变量。

#include 
//using namespace std;//放开std命名空间中的所有变量、函数、类型等
using std::cout;
using std::endl;

int main()
{
	cout << "hello world" << endl;
	return 0;
}

第三种方式便是使用作用域限定符。

#include 
//using namespace std;//放开std命名空间中的所有变量、函数、类型等
//using std::cout;
//using std::endl;

int main()
{
	std::cout << "hello world" << std::endl;
	return 0;
}

总结:
命名空间的使用有三种方式。
1.全部放开
2.局部放开
3.不放开,使用作用域限定符

2.缺省参数 2.1缺省参数概念与分类

缺省参数是明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

#include 
using namespace std;

void test(int x = 10)
{
	cout << x << endl;
}
int main()
{
	test();//没有指定实参
	test(80);//指定实参
	return 0;
}


可以看到打印结果,当我们没有指定实参时,test函数会将形参初始化为10;而当我们指定实参时,test函数的形参便是我们实参的值。

当我们的函数有多个形参时,我们可以让它的形参全部缺省,也可以部分缺省。但部分缺省参数必须是从右往左连续;实参的传递必须是从左往右连续;并且缺省值必须为常量或全局变量

#include 
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);//此时只存在形参z缺省,不能让形参y为缺省参数

	test2();//形参x并不是缺省参数,语法错误
	test2(100);//给定形参x的值,形参y和z缺省

	return 0;
}

总结:
1.缺省参数是声明或定义函数时为函数的参数指定一个缺省值
2.部分缺省参数必须是从右往左连续;实参的传递必须是从左往右连续;
3.缺省值必须为常量或全局变量

2.2缺省参数的使用

缺省参数的实际应用在于我们的项目当中。例如在我们使用C语言实现通讯录当中的初始化函数。函数自动将通讯录的人数初始化为0,在以后的添加联系人的过程当中存储空间呈2倍增长,这无疑是空间的浪费。例如当我们确定我们的通讯录有100个人,而在存储的过程当中空间会被开到能够存储128人的空间。所以缺省参数就能解决这一空间浪费的问题。

void ContactInit(int size = 0)
{
	//函数具体内容
	;
}
int main()
{
	ContactInit();//当我们不确定通讯录的联系人个数时
	ContactInit(100);//当我们确定通讯录的联系人个数为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 
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 
using namespace std;

void Print(int x = 10)
{
	cout << x << endl;
}

void Print()
{
	cout << "hello world" << endl;
}

int main()
{
	Print(20);
	Print();//如果实参为空,那么会调用哪个Print函数呢?
	return 0;
}
3.4C++为什么支持函数重载

在C/C++程序编译链接的过程中,会生成符号表,不同的的编译器会生成不同的符号表。在链接的过程中去寻找符号表对应的函数

通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。同时也确定了返回值的类型不同也不能构成重载,因为二者的编译器都没有对返回值进行修饰。

总结:
1.C与C++的编译器对函数修饰的规则不同
2.C没有办法区分同名的函数,所以不支持重载
3.C++可以区分同名的函数,构成函数重载

4.引用 4.1引用的概念

我们先关注一下引用的实例:

#include 
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;//即使是对rrn取别名,但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)//实体b的引用权限被放大,报错
{
	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();//为什么ret能够接收n的值?
	return 0;
}

在函数栈帧中,当维护函数的两个寄存器指针不再维护这个栈帧时,函数内定义的临时变量便会销毁。那么返回值将被放在寄存器当中

但是刚才我们说了,引用不占用内存空间,引用作返回值寄存器是不工作的,那么就会出现下面这个程序的问题:

#include 
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;//x发生隐式转换,产生一个int类型的临时变量
	return 0;
}

int main()
{
	double x = 2.55;
	int& rx = x;//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. 引用比指针使用起来相对更安全

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/2991417.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-23
下一篇 2022-09-23

发表评论

登录后才能评论

评论列表(0条)

保存