上一篇博客我们正式进入C++的学习,这一篇博客我们继续学习C++入门的基础内容,一定要学好入门阶段的内容,这是后续学习C++的基础,方便我们后续更加容易的理解C++。
目录
一、内联函数
1.0 产生的原因
1.1 概念
1.2 特性
1.3 面试题
二、缺省参数
2.1 缺省参数的概念
2.2 缺省参数的分类
2.2.1 全缺省参数
2.2.2 半缺省参数
2.3 多文件结构的缺省参数函数
2.3.1 缺省参数补充
三、函数重载(重点)
3.0 函数重载的引入
3.1 函数重载概念
3.2 判断函数重载的规则(编译器的工作)
3.3 函数重载解析的步骤
3.4 函数重载判断练习
3.5 C++支持函数重载的原理-名字修饰(name Mangling)
3.5.1 预备知识
3.5.2 C语言编译时函数名修饰约定规则(Windows平台下)
3.5.2 C语言编译时函数名修饰约定规则(Linux平台下)
3.5.3 C++编译时函数名修饰约定规则(Windows平台下)
3.5.3 C++编译时函数名修饰约定规则(Linux平台下)
3.6 C++函数重载的名字粉碎(名字修饰)
3.7 如何指定函数以C方式修饰函数名还是以C++方式修饰函数名
四、函数模板(重点)
4.0 产生的原因
4.1 函数模板定义
4.2 数组的推演及引用的推演
4.3 利用函数模板实现泛型编程
4.4 模板函数的重载与特化(完全特化、部分特化、泛化)
4.5 类模板
五、名字空间:namespace
5.1 C++作用域的划分
5.2 命名空间
5.3 命名空间的定义
5.4 命名空间使用
5.4.1 加命名空间名称及作用域限定符
5.4.2 使用using将命名空间中某个成员引入
5.4.3 使用using namespace 命名空间名称引入
一、内联函数
1.0 产生的原因
当程序执行函数调用时,系统要建立栈空间,保护现场,传递参数以及控制程序执行的转移等等, 这些工作需要系统时间和空间的开销。当函数功能简单,使用频率很高,为了提高效率,直接将函数的代码嵌入到程序中。但这个办法有缺点,一是相同代码重复书写,二是程序可读性往往没有使用函数的好。因此,便产生了内联函数,它的作用主要如下:
1、减少函数调用开销:函数调用通常会有一些额外的开销,包括压栈、跳转、返回等。这些开销对于频繁调用的小函数(如访问器函数、简单的计算函数)而言,可能会显得过高。通过将这些小函数定义为内联函数,可以避免这些开销,因为内联函数会在编译时直接将函数代码插入到调用点。
2、增强代码可读性:通过内联函数,可以将一些频繁使用的代码块抽象为函数,从而提高代码的可读性和可维护性。同时,因为内联函数在编译时会被展开,所以在运行时不会引入函数调用的开销。
#include<ctype.h> //C语言中的字符处理函数库
#include<iostream>
using namespace std;
boo1 IsNumber(char ch)
{
return ch >= 'O' && ch <= '9' ? 1 : 0;
//return isdigit(ch) ;
}
int main()
{
char ch;
while (cin.get(ch), ch != '\n')
{
if (IsNumber(ch))
{
cout << " 是数字字符" <<endl;
}
else
{
cout << "不是数字字符 " <<endl;
}
}
return 0;
}
如果上述代码判断数字字符函数在程序频繁调用,那么将会产生一笔不小的空间开销和时间开销。为了协调好效率和可读性之间的矛盾,C++提供 了另一种方法,即定义内联函数。
1.1 概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开(在编译期间编译器会用函数体替换函数的调用。),没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
查看方式:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2019的设置方式)
1.2 特性
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建 议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为 《C++prime》第五版关于inline的建议:
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
哪什么情况下采用inline处理合适,什么情况下以普通函数形式处理合适呢?这里有个建议给大家。
如果函数的执行开销小于开栈清栈开销(函数体较小),使用inline处理效率高。如果函数的执行开销大于开栈清栈开销,使用普通函数方式处理。
1.3 面试题
内联函数与宏定义区别:
- 内联函数在编译时展开,带参的宏在预编译时展开。
- 内联函数直接嵌入到目标代码中,带参的宏是简单的做文本替换。
- 内联函数有类型检测、语法判断等功能,宏只是替换。
二、缺省参数
2.1 缺省参数的概念
一般情况下,函数调用时的实参个数应与形参相同,但为了更方便地使用函数,C++也允许定义具有缺省参数的函数,这种函数调用时,实参个数可以与形参不相同。缺省参数指在定义函数时为形参指定缺省值(默认值)。这样的函数在调用时,对于缺省参数,可以给出实参值,也可以不给出参数值。如果给出实参,将实参传递给形参进行调用,如果不给出实参,则按缺省值进行调用。
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
2.2 缺省参数的分类
2.2.1 全缺省参数
2.2.2 半缺省参数
注意事项:
缺省参数可以有多个,但所有缺省参数必须放在参数表的右侧,即先定义所有的非缺省参数,再定义缺省参数(半缺省参数必须从右往左依次来给出,不能间隔着给)。这是因为在函数调用时,参数自左向右逐个匹配(函数调用时同样从左往右给实参,不能间隔着给),当实参和形参个数不一致时只有这样才不会产生二义性。
2.3 多文件结构的缺省参数函数
函数的原型(声明):由返回值类型+函数名+形参表,形参名称可以省略,但必须有形参类型
指针变量参数为缺省值的时候,指针变量名不可以省略!
缺省参数不能在函数声明和定义中同时出现,因为如果函数的声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值!
习惯上,缺省参数在公共头文件包含的函数声明中指定, 不要函数的定义中指定。如果在函数的定义中指定缺省参数值,在公共头文件包含的函数声明中不能再次指定缺省参数
值。
2.3.1 缺省参数补充
缺省实参不一定必须是常量表达式,可以使用任意表达式。当缺省实参是一个表达式时在函数被调用时该表达式被求值。
C语言不支持缺省参数(编译器不支持)
三、函数重载(重点)
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。 比如:以前有一个笑话,中国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
3.0 函数重载的引入
C语言实现int, double,char类型的比较大小函数。如下:
int my_max_i(int a, int b)
{
return a > b ? a : b;
}
double my_max_d(double a, double b)
{
return a > b ? a : b;
}
char my_max_c(char a, char b)
{
return a > b ? a : b;
}
很容易发现它们的共同特点:这些函数都执行了相同的一般性动作; 都返回两个形参中的最大值;从用户的角度来看,只有一种操作,就是判断最大值,至于怎样完成其细节,用户一点也不关心。这种词汇上的复杂性不是”判断参数中的最大值“问题本身固有的,而是反映了程序设计环境的一种局限性:在同一个作用域中出现的函数名字必须指向一个唯实体(函数体)。这种复杂性给程序员带来了一个实际问题,他们必须记住或查找每一个函数名字。函数重载把程序员从这种词汇复杂性中解放出来。
#include<iostream>
using namespace std;
int max(int a, int b)
{
return a > b ? a : b;
}
double max(double a, double b)
{
return a > b ? a : b;
}
char max(char a, char b)
{
return a > b ? a : b;
}
int main()
{
cout << max(12, 23) << endl;
cout << max(12.23, 23.45) << endl;
cout << max('a', 'b') << endl;
编译器在编译的时候就已经确定数据类型,从而匹配相应的函数
return 0;
}
3.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
#include<iostream>
using namespace std;
// 1、参数类型不同构成函数重载
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同构成函数重载
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 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()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
编译器在编译的时候就已经确定数据类型,从而匹配相应的函数
return 0;
}
编译器的工作:
当一个函数名在同一个域中被声明多次时,编译器按如下步骤解释第二个(以及后续的)的声明。如果两个函数的参数表中参数的个数或类型或顺序不同,则认为这两个函数是重载。而不认为是函数的重复声明(编译器报错!)。
3.2 判断函数重载的规则(编译器的工作)
这一小节来学习,编译器是如何识别函数重载的,只有我们明白了编译器判断的规则,我们才能正确使用函数重载!
1.如果两个函数的参数表相同,但是返回类型不同, 会被标记为编译错误:函数的重复声明。
int my_max(int a, int b)
{
return a > b ? a : b;
}
unsigned int my_max(int a, int b) // 编译报错,被认为是函数的重复声明;
{
return a > b ? a : b;
}
int main()
{
int ix = my_max(12, 23);
unsigned int =my_max(12, 23); // 编译无法通过;
reutrn 0;
}
为什么呢?
函数的返回类型不能作为函数重载的依据,编译器区分函数是依靠函数声明中的函数名和参数的类型,编译器不知道应该调用哪个函数,编译器认为是同一个函数重复声明;
2.参数表的比较过程与形参名无关。
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
int my_add(int a, int b);
int my_add(int x, int y);
为什么呢?
函数的参数列表中的参数名不能作为函数重载的依据,同样,因为编译器不以参数名称来区分函数,被认为是同一个函数,编译器认为是同一个函数重复声明;
3.如果在两个函数的参数表中,只有缺省实参不同,编译器同样认为是同一个函数重复声明;也就是说它认为这是一个函数。
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
void Print(int* br, int n);
void Print(int* br, int len = 10);
4.typedef名为现有的数据类型提供了一个别名,它并没有创建一个新类型,因此,如果两个函数参数表的区别只在于一个使用了typedef重命名,而另一个使用了与typedef相应的类型。则该参数表被视为相同的参数列表,编译器同样认为是同一个函数重复声明;也就是说它认为这是一个函数。
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
typedef unsigned int u_int;
int Print(u_int a);
int Print(unsigned int b);
5.当一个形参类型有const或volatile修饰时,如果形参是按值传递方式定义,在识别函数声明是否相同时,并不考虑const和volatile修饰符.(不考虑CV特性)
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
void fun(int a);
void fun(const int a);
6.当一个形参类型有const或volatile修饰时,如果形参是按照指针或引用定义时,在识别函数声明是否相同时,就要考虑const和volatile修饰符.(考虑CV特性),那么又该如何考虑呢?
#include<iostream>
using namespace std;
void print(int &a)
{
cout<<"左值引用"<<endl;
}
void print(const int &b)
{
cout<<"常性左值引用(万能引用)"<<endl;
}
void print(int &&b)
{
cout<<"右值引用"<<endl;
}
int main()
{
int x = 10;
print(x); //优先匹配左值引用
const int y = 10;
print(y); //只匹配常性左值引用/常引用!(万能引用)
print(10); //优先匹配右值引用
return 0;
}
匹配规则如下:
首先,C++只有三种引用方式:左值引用(引用的是左值:可以取地址)、常性左值引用/万能引用(既可以引用左值:可以取地址,又可以引用右值:不可以取地址)、右值引用(引用的是右值:不可以取地址),当存在三个引用的函数时,我们应明确编译器的匹配顺序,根据实参的类型进行匹配!!!
对于左值,优先匹配左值引用,其次才是常性左值引用!
对于常性左值,直接匹配常性左值引用,不存在,编译器直接报错!
对于右值引用,优先匹配右值引用,其次才是常性左值引用!
- 对于普通的变量(它是左值),编译器首先优先匹配形参为左值引用的函数,如果不存在该函数,然后,匹配形参为常性左值引用(常引用)的函数,如果二者都不存在,编译器直接报错!
- 对于常变量(const修饰),编译器直接匹配形参为常性左值引用(常引用)的函数,如果该函数不存在,编译器直接报错(不能匹配参数为右值引用的函数,右值引用引用的是右值)!
- 对于右值,编译器首先优先匹配形参为右值引用的函数,如果该函数不存在,然后,匹配形参为常性左值引用(常引用)的函数,如果二者都不存在,编译器直接报错!
7.注意函数调用的二义性; 如果在两个函数的参数表中,形参类型相同,而形参个数不同,形参默认值将会影响函数的重载.
#include<iostream>
using namespace std;
/****函数调用的二义性****/
void fun(int a);
void fun(int a, int b);
void fun(int a, int b = 10);
int main()
{
fun(12); //这里第一个函数会和第三个函数会发生冲突,编译器不知道该调用哪一个函数
fun(12,13); //这里第二个函数会和第三个函数会发生冲突,编译器不知道该调用哪一个函数
return 0;
}
3.3 函数重载解析的步骤
- 确定函数调用考虑的重载函数的集合,确定函数调用中实参表的属性(由形参表的属性确定调用那个函数)。
- 从重载函数集合中选择函数,该函数可以在(给出实参个数和类型)的情况下可以调用函数。
- 选择与调用最匹配的函数。
3.4 函数重载判断练习
#include<iostream>
using namespace std;
* /*******下面两个函数构成重载***********/
void func(int *p) {} //可读可写
void func(const int *p) {} //只可读,不可解引用修改
int main()
{
int x = 10; //普通变量:可读可写
func(&x); //优先匹配普通指针的函数(第一个),其次才是const修饰的指针的函数(第二个)
const int y = 10; //常变量:只可读
func(&y); //只能匹配const修饰的指针的函数(第二个),如果不存在该函数,直接报错!
return 0;
}
#include<iostream>
using namespace std;
/*******下面两个函数不构成重载***********/
//编译器会报错,这不是函数的重载,第二个在编译的时候会直接忽略const ,认为是同一个函数重复声明
void func(int *p) {} //可读可写,可以解引用修改数据
void func( int* const p) {} //可读可写,可以解引用修改数据,const修饰指针自身的时候,编译器可以忽略const的存在
/**如果修改第二个为:void func( const int* const p) {} 那么这便又是函数的重载**********/
int main()
{
int x = 10; //普通变量
func(&x); //两个函数功能一样,对传入的数据都可读可写,匹配两个都可以!编译器不知道调用哪个,就会报错,认为是同一个函数重复声明!
return 0;
}
#endif
#include<iostream>
using namespace std;
/*****下面两个函数是重载函数:参数的个数不同******/
void func(int a);
void func(int a,int b);
int main()
{
func(12);
func(12, 23);
}
#include<iostream>
using namespace std;
void func(int a);
void func(int a, int b=20);
int main()
{
func(12); //这里第一个函数会和第二个函数会发生冲突,编译器不知道该调用哪一个函数
func(12, 23);//这里第一个函数会和第二个函数会发生冲突,编译器不知道该调用哪一个函数
}
由于函数调用的二义性,不是重载函数,这种问题只要不调用函数就不会报错,但是只要调用就会出现编译错误!!!
3.5 C++支持函数重载的原理-名字修饰(name Mangling)
3.5.1 预备知识
“C"或者"C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者
原型时生成的字符串。修饰名由函数名、类名、调用约定、返回值类型、参数表共同决定。不同的调用方式,形成的修饰名也不同。
- _stdcall调用:Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。
- C调用(即用_cdecl 关键字说明):按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。
- _fastcall 调用:"人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字( DWORD )或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
- thiscall调用:仅仅应用于“C++ "类的成员函数。this 指针存放于ECX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
- 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们 可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
- 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
- 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的 函数名修饰规则。
3.5.2 C语言编译时函数名修饰约定规则(Windows平台下)
C语言的名字修饰规则非常简单,_ cdec是C/C++的缺省调用方式,调用约定函数名字前面添加了下划线前缀。
_stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@”符号和其参数的字节数。
_fastcall调用约定在输出函数名前加上一个” @ "符号,函数名后面也是一个" @ "符号和其参数的字节数。
3.5.2 C语言编译时函数名修饰约定规则(Linux平台下)
通过下面我们可以看出gcc的函数修饰后名字不变。
3.5.3 C++编译时函数名修饰约定规则(Windows平台下)
3.5.3 C++编译时函数名修饰约定规则(Linux平台下)
3.6 C++函数重载的名字粉碎(名字修饰)
面试题:
编译器编译完是按照修饰名访问这些函数的,不同的调用方式,编译器编译时函数名修饰约定规则不同。
1、什么是函数重载?
函数名相同而参数列表不同(类型或者数量),叫函数重载!
2、返回值可不可以作为函数重载的依据?
不可以!C++的函数调用解析机制仅基于参数类型和数量来匹配函数,而不考虑返回值类型!如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
3、为什么C++可以进行函数的重载,而C语言不可以?
原因在于:二者的修饰名规则不同!(名字粉碎技术不同), C语言只是在函数名的 前面加个下划线,那么对于多个同名函数,他们的修饰名都相同,都是在函数名前面加个下划线!编译器在编译阶段无法区分!C++把形参列表的类型和数量作为修饰名的一部分,这样相同的函数名(函数重载)就会得到不同的修饰名,这样编译器在编译阶段就可以区分出来!
3.7 如何指定函数以C方式修饰函数名还是以C++方式修饰函数名
extern "C" int add(int x, int y) { return x + y; } //<==> 按照C的修饰规则,修饰名为:_add
double add(double x, double y) { return x + y; }//<==> 按照C++的修饰规则,修饰名为:?add@@YAHHH@Z
//上面编译可以通过,因为C和C++修饰名规则不同,编译器可以区分!
extern "C" int add(int x, int y) { return x + y; } //<==>按照C的修饰规则,修饰名为:_add
extern "C" double add(double x, double y) { return x + y; } //<==>按照C的修饰规则,修饰名为:_add
//上面编译不可以通过,因为二者使用的都是C的修饰名规则,编译器无法区分!
//如何将一批函数指定相同的修饰规则:加个大括号
extern "C"
{
int add(int x, int y) //<==> 按照C的修饰规则,修饰名为:_add
{
return x + y;
}
double add(double x, double y) //<==> 按照C的修饰规则,修饰名为:_add
{
return x + y;
}
}
四、函数模板(重点)
4.0 产生的原因
为了代码重用, 代码就必须是通用的;通用的代码就必须不受数据类型的限制。那么我们可以把
数据类型改为一个设计参数。这种类型的程序设计称为参数化(parameterize) 程序设计。
软件模块由模板构造。包括函数模板(function template)和类模板(class template)。
函数模板可以用来创建一个通用功能的函数, 以支持多种不同形参, 简化重载函数的设计。
4.1 函数模板定义
<模板参数表> 尖括号中不能为空,参数可以有多个,用逗号分开。模板参数主要是模板类型参数。模板类型参数代表一种类型,由关键字class 或typename 后加一个标识符构成,在这里两个关键字的意义相同,它们表示后面的参数名代表一个潜在的内置或用户设计的类型。
#include<iostream>
using namespace std;
template<class T>
T my_max(T a,T b)
{
return a > b ? a : b;
}
int main()
{
my_max(12, 23);
my_max('a', 'b');
my_max(12.23, 34.45);
return 0;
}
编译阶段,编译器根据实参类型自动的生成如下函数代码:这也叫模板实参推演,其本质上还是函数重载,由编译器自动生成代码。
typedef int T;
T my_max<int>(T a, T b)
{
return a > b ? a : b;
}
typedef char T;
T my_max<char>(T a, T b)
{
return a > b ? a : b;
}
typedef double T;
T my_max<double>(T a, T b)
{
return a > b ? a : b;
}
在编译过程中,根据函数模板的实参构造出独立的函数,称为模板函数。这个构造过程被称为模板实例化。
#if 0
#include<iostream>
using namespace std;
#include <typeinfo>
template<class T>
void print(T a)
{
cout << typeid(a).name() << endl;
cout << typeid(a).name() << endl;
cout << typeid(a).name() << endl;
}
int main()
{
int a = 10;
int arr[] = { 1,2,3,4,5 };
print(a); //编译阶段,编译器推演出:T为整型 int
print(&a); //编译阶段,编译器推演出:T为整型指针 int *
print(arr); //数组名代表首元素的地址,也就是一个指针,编译阶段,编译器推演出:T为整型指针 int *
}
#endif
4.2 数组的推演及引用的推演
#if 1
#include<iostream>
using namespace std;
#include <typeinfo>
template<class T>
void print(T &a) //函数的参数为实参类型的引用(实参的引用)
{
cout << typeid(T).name() << endl;
cout << typeid(a).name() << endl;
cout << sizeof(a) << endl; //20
}
int main()
{
int a = 10;
int arr[] = { 1,2,3,4,5 };
print(a); //编译阶段,实参a为一个整型, 编译器推演出:T为整型 int ,函数参数a为整型引用类型:int &
print(&a); //编译阶段,编译器无法推演,实参为一个整型指针也就是:int *, 所以a就是这个整型指针的引用,按道理推演a为:int* & ,在实参里面可以a++,但是这里的&a是一个地址,是一个常量(带有常性const),所以编译器无法推演!,
print(arr); //编译阶段,数组名代表首元素的地址,也就是一个整型指针,编译阶段,编译器推演出:数组引用,int [5]&
}
4.3 利用函数模板实现泛型编程
下面这个打印数组的函数代码适用于一切类型的数组,我都可以打印整个数组,类型由编译器自动推演
#include<iostream>
using namespace std;
// typedef int T ==>int
// #define N 5 ==>5
template<class T, int N>
void print(T(&br)[N]) /*模板类型参数推演时:类型是重命名规则,非类型是宏的规则直接拿数值替换*/
{
for (int i = 0; i < N; i++)
{
cout << br[i] << " ";
}
count << endl;
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
double brr[3] = { 1.2,3.4,5.3 };
print(arr); //数组引用:int (&br)[5]=arr;
print(brr); //数组引用:double (&br)[3]=brr;
}
4.4 模板函数的重载与特化(完全特化、部分特化、泛化)
- 模板函数的重载是C++中允许通过模板实现多个函数版本的功能。在C++中,函数可以通过相同的名字但不同的参数列表来重载。模板函数也可以通过这种方式实现重载。
- 函数模板特化是指为特定类型提供一个专门的模板实现,与上面相比,而不是使用通用的模板版本。
#include<iostream>
using namespace std;
/*泛化:无任何限制*/
template<class T>
void func(T a)
{
}
/*部分特化:限制常性的任意类型指针*/
template<class T>
void func(const T *p)
{
}
/*完全特化:限制常性的字符类型指针*/
void func(const char* p)
{
}
int main()
{
const char* str = "hello";
int a = 10;
func(a);
func(&a);
func(str);
return 0;
}
4.5 类模板
类模板是C++中一种强大的工具,用于定义通用的类。通过类模板,可以创建能够处理任意数据类型的类,而无需为每种数据类型编写单独的类。类模板使用模板参数来表示类中的数据类型,使得类的实现能够适用于多种类型。容器是一种数据结构,用于存储和管理一组对象。在C++标准库(STL,Standard Template Library)中,容器是实现了特定接口的类模板。这些类模板提供了存储、访问和操作其包含的元素的功能。
C++容器库包括序列容器、关联容器和无序容器。
序列容器(Sequence Containers):用于按照线性顺序存储数据。
- std::vector:动态数组,支持快速随机访问和在末尾插入 / 删除元素。
- std::deque:双端队列,支持快速随机访问和在两端插入 / 删除元素。
- std::list:双向链表,支持在任何位置快速插入 / 删除元素。
- std::array:固定大小的数组,大小在编译时确定。
- std::forward_list:单向链表,支持在任何位置快速插入 / 删除元素,但只允许单向遍历。
容器库的特点
- 泛型编程:通过模板实现,容器可以存储任意类型的对象。
- 自动管理内存:容器会自动管理其所需的内存,开发者无需手动分配和释放内存。
- 迭代器支持:所有容器都提供迭代器,用于遍历和操作元素。
- 算法兼容性:STL中的算法可以与容器无缝配合使用,提供诸如排序、搜索、复制等操作。
使用类模板生成任意类型的顺序栈
template<class T>
class SeqStack
{
private:
T* data;
int top;
public:
SeqStack(int sz = 100)
{
data = (T*)malloc(sizeof(T) * sz);
top = -1;
}
};
int main()
{
SeqStack<int> ist; //类模板的类型必须给定
SeqStack<double> dst;
}
/*******编译器会根据上述模板生成如下代码:**/
class SeqStack<int>
{
typedef int T;
private:
T* data;
int top;
public:
SeqStack(int sz = 100)
{
data = (T*)malloc(sizeof(T) * sz);
top = -1;
}
};
class SeqStack<double>
{
typedef double T;
private:
T* data;
int top;
public:
SeqStack(int sz = 100)
{
data = (T*)malloc(sizeof(T) * sz);
top = -1;
}
};
五、名字空间:namespace
5.1 C++作用域的划分
在C++中把作用域划分为:全局作用域,局部作用域、块作用域,名字空间作用域和类作用域。注意:作用域是针对编译器来说的,生存周期针对运行的时候来说的。函数被调用时,被调用函数内部的变量才会分配内存空间,当函数执行完毕,被调用函数内部的变量将会归还给操作系统。我们定义的变量存储在栈区或者堆区。
- 全局变量:函数之外定义的变量:存储在数据区,
- 局部变量:函数内部定义的变量:存储在栈区,
- 静态局部变量:函数内部定义的变量加static关键字修饰,只创建一次,保存上次的值,不会重新初始化:存储在数据区(字符串常量也是)
- 花括号内部的变量,只在花括号内部有效:存储在栈区。
- 类作用域是指在类定义内部的作用域,决定了类中的成员(包括成员变量和成员函数)的可见性和生命周期。
- 名字空间作用域:多文件编程时:多个源文件定义相同的全局变量名字或者函数名,在项目进行编译链接时候就会发生全局命名冲突!名字空间域是随标准C++而引入的。它相当于一个更加灵活的文件域(全局域)。
5.2 命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存 在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
名字空间域的引入,主要是为了解决全局名字空间污染(global namespace pollution)问题,即防止程序中的全局实体名与其他程序中的全局实体名的命名冲突。于是,便产生了命名空间。
5.3 命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{} 中即为命名空间的成员。
// 1. 正常的命名空间定义
namespace p1
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
//2. 命名空间可以嵌套
// test.cpp
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;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
5.4 命名空间使用
命名空间中成员该如何使用呢?比如:
5.4.1 加命名空间名称及作用域限定符
int main()
{
int a = yhp: :my_ add(12,23) ;
printf("%1f \n",Primer: :pi);
printf ("%f \n",yhp: :pi);
Primer: :Matrix: :my_ _max('a','b');
return 0;
}
5.4.2 使用using将命名空间中某个成员引入
using yhp::pi;
using Primer: :Matrix: :my_max;
//名字空间类成员matrix的using声明
//以后在程序中使用matrix时,就可以直接使用成员名,而不必使用限定修饰名。
int main()
{
printf("%1f \n",Primer: :pi) ;
printf("%f \n",pi); // yhp::
my_max('a','b');
return 0;
}
5.4.3 使用using namespace 命名空间名称引入
使用using指示符可以一次性地使名字空间中所有成员都可以直接被使用,比using声明方便。using指示符;以关键字using开头,后面是关键字namespace,然后是名字空间名。
using namespace名字空间名;
using namespace yhp;
int main()
{
printf("%1f n",Primer: :pi ) ;
printf("%f n" ,pi) ;// yhp: :
my_add(12,23); // yhp::
return 0;
}
多文件结构示例
std命名空间的使用惯例: .
std是C++标准库的命名空间,如何展开std使用更合理呢?
1.在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对
象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模
大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间+
using std::cout展开常用的库对象/类型等方式。
这篇博客内容较为丰富,为后续学习好C++做好准备, 如果对此专栏感兴趣,点赞加关注!