C++基础细碎知识整理

本文详细介绍了C++中的命名空间,解释了其作用和使用方式,包括不展开、部分展开和全展开的优缺点。接着讨论了函数重载的概念,规则以及C++如何实现重载,并提醒了全缺省参数和半缺省参数的注意事项。最后,文章讲解了引用的使用场景及其规则,并探讨了内联函数和宏的优缺点。
摘要由CSDN通过智能技术生成

目录

命名空间的使用

来源

命名空间的使用

不展开

部分展开

全展开

函数重载

函数重载的规则

C++如何实现函数重载

引用


命名空间的使用

来源

在了解命名空间的原理和使用之前,我们先要理解,命名空间是为了解决什么问题。

C++是在C的基础上发展而形成的一种语言,完全兼容C的语法,也加入了许多新的规则和语法来解决C的缺陷。

命名空间就是为了解决C语言中的重复命名的问题。

首先我们来看看这么一个代码:

#include<stdio.h>
int main()
{
	int scanf = 20;
	printf("%d", scanf);
	return 0;
}

我们都知道scanf在C之中是一个函数名,但诡异的是我们在主函数中声明scanf是有效的并且输出结果是20.

在这个程序内的scanf就是表示是一个int型整数,这是根据就近原则或者说是局部优先原则确定的。

接下来我们看看另一个程序:

#include<stdio.h>
int main()
{
	int scanf = 20;
	scanf("%d", &scanf);
	printf("%d", scanf);
	return 0;
}

在我们想要scanf作为函数使用时出现了问题,两者之间命名冲突。

在C语言中我们被告诫不要让变量名与函数名冲突,但是在C++中有没有合适的解决方法呢?

命名空间的使用

我们先来书写一个在C++中最为简单的程序:

#include<iostream>

using namespace std;

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

这里我们就看到了namespace命名空间,但是现在它是用来干什么的我们还不清楚,首先我们先来了解一下它。

一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。

让我们从代码方面来看看命名空间到底是什么:

#include<iostream>

using namespace std;
namespace  N1
{
	int printf = 30;
	int strlen = 20;
}
int main()
{
	cout << "hello  world" << endl;
	cout << N1::printf << endl;
	return 0;
}

我们声明了一个命名空间N1,在内部声明了两个int类型的变量,通过作用域限定符::我们就可以调用命名空间中的变量。

并且命名空间中也可以嵌套命名空间:

namespace  N1
{
	int printf = 30;
	int strlen = 20;
	namespace  N2
	{
		int a = 0;
	}
}

通过上面的解释,我们明白了,命名空间适用于将声明的名称之间相互隔离,防止命名冲突。比如说,我们调用N1::printf时,::将范围限定在N1这个命名空间之中,而不会与函数名printf冲突,反之亦然。
那么一开始我们看到的那个程序是什么意思呢?

我们先来看另一个版本的程序:

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

显然std也是一个命名空间,通过作用域限定符调用命名空间std内的内容。

那么一开始的

using namespace std;

是用来干嘛的呢?

using的作用是把命名空间中的内容在全局空间中展开,命名空间中的变量就成为了全局变量,调用时就不需要命名空间名加上作用域限定符了。

实际上命名空间有三种使用方式,各有优劣。

不展开

也就是

std::cout << "hello  world" << std::endl;

的方式,要使用命名空间中的名称,就要使用::来限定命名空间,完全避免了冲突,在大工程中使用。缺点就是在日常练习中书写代码较为繁琐。

部分展开

使用using将命名空间中成员引入,将一些我们常用的符号在全局中展开,就可以大大简化代码,是在第一个方法和第三个方法之间取一个折中。

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

全展开

使用using namespace 命名空间名称引入:

using namespace std;
int main()
{
	cout << "hello  world" << endl;
	//cout << N1::printf << endl;
	return 0;
}

这个方法是有问题的,相当于一夜回到解放前。好不容易搞个命名空间隔离了,结果一行代码全给展开了,直接在全局空间声明,完全没有防止命名冲突的作用,只在日常练习中使用。
 

缺省参数

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

void Func(int a = 0)
{
    cout<<a<<endl;
}
int main()
{
    Func(); // 没有传参时,使用参数的默认值
    Func(10); // 传参时,使用指定的实参
    return 0;
}

从结果可以看到,调用函数时如果不提供实参,使用缺省值,如果提供实参,则使用实参。 

缺省参数分类

缺省参数可以分为全缺省参数和半缺省参数。

全缺省参数——所有形参都带有缺省值:

void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}

半缺省参数——部分形参都带有缺省值:

void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}

注意点:

1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现,一般在声明中给出

//a.h
void Func(int a = 10);
// a.cpp
void Func(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该
用那个缺省值。

3. 缺省值必须是常量或者全局变量
4. C语言不支持(编译器不支持)
 

函数重载

函数重载的规则

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数 个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。

比如下面的相加函数:

int Add(int left, int right)
{
return left+right;
}

double Add(double left, double right)
{
return left+right;
}

long Add(long left, long right)
{
return left+right;
}


int main()
{
Add(10, 20);
Add(10.0, 20.0);
Add(10L, 20L);
return 0;
}

虽然函数名相同,但是所带参数的类型不同,所以在调用的时候能够调用不同的函数定义,让函数的使用更加灵活。

这里要特别注意的是函数重载的规则:同名函数的形参列表(参数 个数 或 类型 或 顺序)必须不同。

short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}

那么两个同名函数,算不算是函数重载呢,显然他们的形式参数的个数 ,类型以及 顺序都是一样的,只是返回类型不同,不构成重载函数。

C++如何实现函数重载

我们都知道C是没有函数重载这个功能的,那么C++是怎么实现的呢?

其实是通过C++的函数名修饰来实现函数重载的。

大家这里可能有一些迷糊,这需要我们对代码的编译过程有一定的了解:

C和C++的区别在于,在编译的符号汇总中,C语言是使用函数的原名进行汇总的,导致了一个名称只能对应一个函数,所以不能进行函数重载。

但是在C++中,符号汇总起来的是经过修饰的函数名,即使原名称一样,由于参数 个数 或 类型 或 顺序不同,经过修饰后的符号名也不同,比如说:

int Add(int left, int right)
{
return left+right;
}

double Add(double left, double right)
{
return left+right;
}

long Add(long left, long right)
{
return left+right;
}

 比如说这三个函数经过函数名修饰之后就变成了——Addii、Adddd、Addll。于是我们在后续的使用中就可以很好地区分这三个函数了,即使在我们看来这三个函数名是一样的,但是在计算机看来这三个完全就是三个不一样的函数。

函数返回类型不同

如果函数的返回值不同是否能构成函数重载?

int Add(int left, int right)
{
return left+right;
}

char Add(int left, int right)
{
return left+right;
}

如果函数名修饰包含函数返回类型,上述代码中的两个函数能重载吗?

答案是不能,不管函数名修饰是否包括函数名,在调用函数时,是无法从语句中判断调用函数的返回类型的,这就是二义性,编译器无法区分。

函数重载和缺省结合之后的小问题

int func(int a = 0, int b = 10)
{
}

int func()
{}

上面两个函数,构成了重载函数,但是在无参调用时发生了冲突,不知道调用哪个好,这也是调用中的二义性。

引用

引用可以说就是给变量取别名。是怎么实现的呢?

#include<iostream>

int main()
{
	int a = 0;
	int b = 0;
	int& c = a;
	c = 10;
	return 0;
}

其中 int& c = a; 就是把c作为a的别名,这时c和a指向的内存空间是同一块。

 当改变c的值时,a中的值同时改变。

 但是,在使用过程中也有一些需要注意的事项——

1.引用类型必须和引用实体是同种类型的


 

2.引用在定义时必须初始化

int a;
int& b = a;

 

 3.一个变量可以有多个引用

int a = 0;
int& b = a;
int& c = a;
int$ d = c;

 4.引用一旦引用一个实体,再不能引用其他实体

#include<iostream>

int main()
{
	int a = 0;
	int b = 0;
	int& c = a;
	int& c = b;
	c = 10;
	return 0;
}

引用使用场景

做参数——输出型参数

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

做返回值:

int& Count()
{
    static int n = 0;
    n++;
    // ...
    return n;
}

我们来看看传值和传引用之间的区别:

传引用不需要再创建一个临时变量,可以提高程序的运行效率,在所传的值特别大的情况下尤其如此。

但是,如果是函数内变量,就不能使用传引用。虽然函数栈帧销毁之后空间还在,但是名义上这块空间你已经没有权限使用了,而且很有可能是别人在用,如何继续读写就出问题了。

如果把内存的申请和释放比作住酒店,把数据的读写比作在房间内存取东西,第二行第一个图的情况就像是已经退房了,但是事先配了一把 房间的钥匙,现在从法律上来说房间已经不是我的了,但是我还是可以往里面存取东西,同时酒店也有可能把这间房间给别人。这种情况下,我可能把别人存的东西销毁,别人也有可能把我存的东西销毁,对双方来说都不安全。

所以内存销毁意味着,空间还在,还能读写,但是读的数据不确定是什么。

所以,注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。

常引用

void TestConstRef()
{
    const int a = 10;
    //int& ra = a; // 该语句编译时会出错,a为常量
    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
}

上述const int& rd = d; 由于类型转换中间产生一个临时变量,这个临时变量具有常性,只能读,不能写,所以使用const  引用修饰。

并且,在底层中,引用就是通过指针来实现的。

内联函数

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调
用建立栈帧的开销,内联函数提升程序运行的效率。

在函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的
调用。
查看方式:
1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
会对代码进行优化,以下给出vs2013的设置方式)


 

 

内联函数的特性 

1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为
《C++prime》第五版关于inline的建议:

 3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。

// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}

// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?
1. 常量定义 换用const enum
2. 短小函数定义 换用内联函数
 

  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JDSZGLLL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值