C++基础(三):C++入门(二)

上一篇博客我们正式进入C++的学习,这一篇博客我们继续学习C++入门的基础内容,一定要学好入门阶段的内容,这是后续学习C++的基础,方便我们后续更加容易的理解C++。

目录

一、内联函数

[1.0 产生的原因](#1.0 产生的原因)

[1.1 概念](#1.1 概念)

[1.2 特性](#1.2 特性)

[1.3 面试题](#1.3 面试题)

二、缺省参数

[2.1 缺省参数的概念](#2.1 缺省参数的概念)

[2.2 缺省参数的分类](#2.2 缺省参数的分类)

[2.2.1 全缺省参数](#2.2.1 全缺省参数)

[2.2.2 半缺省参数](#2.2.2 半缺省参数)

[2.3 多文件结构的缺省参数函数](#2.3 多文件结构的缺省参数函数)

[2.3.1 缺省参数补充](#2.3.1 缺省参数补充)

三、函数重载(重点)

[3.0 函数重载的引入](#3.0 函数重载的引入)

[3.1 函数重载概念](#3.1 函数重载概念)

[3.2 判断函数重载的规则(编译器的工作)](#3.2 判断函数重载的规则(编译器的工作))

[3.3 函数重载解析的步骤](#3.3 函数重载解析的步骤)

[3.4 函数重载判断练习](#3.4 函数重载判断练习)

[3.5 C++支持函数重载的原理-名字修饰(name Mangling)](#3.5 C++支持函数重载的原理-名字修饰(name Mangling))

[3.5.1 预备知识](#3.5.1 预备知识)

[3.5.2 C语言编译时函数名修饰约定规则(Windows平台下)](#3.5.2 C语言编译时函数名修饰约定规则(Windows平台下))

[3.5.2 C语言编译时函数名修饰约定规则(Linux平台下)](#3.5.2 C语言编译时函数名修饰约定规则(Linux平台下))

[3.5.3 C++编译时函数名修饰约定规则(Windows平台下)](#3.5.3 C++编译时函数名修饰约定规则(Windows平台下))

[3.5.3 C++编译时函数名修饰约定规则(Linux平台下)](#3.5.3 C++编译时函数名修饰约定规则(Linux平台下))

[3.6 C++函数重载的名字粉碎(名字修饰)](#3.6 C++函数重载的名字粉碎(名字修饰))

[3.7 如何指定函数以C方式修饰函数名还是以C++方式修饰函数名](#3.7 如何指定函数以C方式修饰函数名还是以C++方式修饰函数名)

四、函数模板(重点)

[4.0 产生的原因](#4.0 产生的原因)

[4.1 函数模板定义](#4.1 函数模板定义)

[4.2 数组的推演及引用的推演](#4.2 数组的推演及引用的推演)

[4.3 利用函数模板实现泛型编程](#4.3 利用函数模板实现泛型编程)

[4.4 模板函数的重载与特化(完全特化、部分特化、泛化)](#4.4 模板函数的重载与特化(完全特化、部分特化、泛化))

[4.5 类模板](#4.5 类模板)

五、名字空间:namespace

[5.1 C++作用域的划分](#5.1 C++作用域的划分)

[5.2 命名空间](#5.2 命名空间)

[5.3 命名空间的定义](#5.3 命名空间的定义)

[5.4 命名空间使用](#5.4 命名空间使用)

[5.4.1 加命名空间名称及作用域限定符](#5.4.1 加命名空间名称及作用域限定符)

[5.4.2 使用using将命名空间中某个成员引入](#5.4.2 使用using将命名空间中某个成员引入)

[5.4.3 使用using namespace 命名空间名称引入](#5.4.3 使用using namespace 命名空间名称引入)


一、内联函数

1.0 产生的原因

当程序执行函数调用时,系统要建立栈空间,保护现场,传递参数以及控制程序执行的转移等等, 这些工作需要系统时间和空间的开销。当函数功能简单,使用频率很高,为了提高效率,直接将函数的代码嵌入到程序中。但这个办法有缺点,一是相同代码重复书写,二是程序可读性往往没有使用函数的好。因此,便产生了内联函数,它的作用主要如下:

  • 1、减少函数调用开销:函数调用通常会有一些额外的开销,包括压栈、跳转、返回等。这些开销对于频繁调用的小函数(如访问器函数、简单的计算函数)而言,可能会显得过高。通过将这些小函数定义为内联函数,可以避免这些开销,因为内联函数会在编译时直接将函数代码插入到调用点。

  • 2、增强代码可读性 :通过内联函数,可以将一些频繁使用的代码块抽象为函数,从而提高代码的可读性和可维护性。同时,因为内联函数在编译时会被展开,所以在运行时不会引入函数调用的开销。

cpp 复制代码
#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++编译器会在调用内联函数的地方展开(在编译期间编译器会用函数体替换函数的调用。,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

查看方式:

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
  2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2019的设置方式)

1.2 特性

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

哪什么情况下采用inline处理合适,什么情况下以普通函数形式处理合适呢?这里有个建议给大家。

如果函数的执行开销小于开栈清栈开销(函数体较小),使用inline处理效率高。如果函数的执行开销大于开栈清栈开销,使用普通函数方式处理。

1.3 面试题

内联函数与宏定义区别:

  1. 内联函数在编译时展开,带参的宏在预编译时展开。
  2. 内联函数直接嵌入到目标代码中,带参的宏是简单的做文本替换。
  3. 内联函数有类型检测、语法判断等功能,宏只是替换。

二、缺省参数

2.1 缺省参数的概念

一般情况下,函数调用时的实参个数应与形参相同,但为了更方便地使用函数,C++也允许定义具有缺省参数的函数,这种函数调用时,实参个数可以与形参不相同。缺省参数指在定义函数时为形参指定缺省值(默认值)。这样的函数在调用时,对于缺省参数,可以给出实参值,也可以不给出参数值。如果给出实参,将实参传递给形参进行调用,如果不给出实参,则按缺省值进行调用。

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

2.2 缺省参数的分类

2.2.1 全缺省参数

2.2.2 半缺省参数

注意事项:

缺省参数可以有多个,但所有缺省参数必须放在参数表的右侧,即先定义所有的非缺省参数,再定义缺省参数(半缺省参数必须从右往左依次来给出,不能间隔着给)。这是因为在函数调用时,参数自左向右逐个匹配(函数调用时同样从左往右给实参,不能间隔着给),当实参和形参个数不一致时只有这样才不会产生二义性。

2.3 多文件结构的缺省参数函数

函数的原型(声明):由返回值类型+函数名+形参表,形参名称可以省略,但必须有形参类型

指针变量参数为缺省值的时候,指针变量名不可以省略!

缺省参数不能在函数声明和定义中同时出现,因为如果函数的声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值!

习惯上,缺省参数在公共头文件包含的函数声明中指定, 不要函数的定义中指定。如果在函数的定义中指定缺省参数值,在公共头文件包含的函数声明中不能再次指定缺省参数
值。

2.3.1 缺省参数补充

缺省实参不一定必须是常量表达式,可以使用任意表达式。当缺省实参是一个表达式时在函数被调用时该表达式被求值。

C语言不支持缺省参数(编译器不支持)

三、函数重载(重点)

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。 比如:以前有一个笑话,中国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是"谁也赢不了!",后者是"谁也赢不了!"

3.0 函数重载的引入

C语言实现int, double,char类型的比较大小函数。如下:

cpp 复制代码
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;
}

很容易发现它们的共同特点:这些函数都执行了相同的一般性动作; 都返回两个形参中的最大值;从用户的角度来看,只有一种操作,就是判断最大值,至于怎样完成其细节,用户一点也不关心。这种词汇上的复杂性不是"判断参数中的最大值"问题本身固有的,而是反映了程序设计环境的一种局限性:在同一个作用域中出现的函数名字必须指向一个唯实体(函数体)。这种复杂性给程序员带来了一个实际问题,他们必须记住或查找每一个函数名字。函数重载把程序员从这种词汇复杂性中解放出来。

cpp 复制代码
#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++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。

cpp 复制代码
#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.如果两个函数的参数表相同,但是返回类型不同, 会被标记为编译错误:函数的重复声明。

cpp 复制代码
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.参数表的比较过程与形参名无关。

cpp 复制代码
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
int my_add(int a, int b);
int my_add(int x, int y);

为什么呢?

函数的参数列表中的参数名不能作为函数重载的依据,同样,因为编译器不以参数名称来区分函数,被认为是同一个函数,编译器认为是同一个函数重复声明;

3.如果在两个函数的参数表中,只有缺省实参不同,编译器同样认为是同一个函数重复声明;也就是说它认为这是一个函数。

cpp 复制代码
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
void Print(int* br, int n);
void Print(int* br, int len = 10);

4.typedef名为现有的数据类型提供了一个别名,它并没有创建一个新类型,因此,如果两个函数参数表的区别只在于一个使用了typedef重命名,而另一个使用了与typedef相应的类型。则该参数表被视为相同的参数列表,编译器同样认为是同一个函数重复声明;也就是说它认为这是一个函数。

cpp 复制代码
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
typedef unsigned int u_int;
int Print(u_int a);
int Print(unsigned int b);

5.当一个形参类型有const或volatile修饰时,如果形参是按值传递方式定义,在识别函数声明是否相同时,并不考虑const和volatile修饰符.(不考虑CV特性)

cpp 复制代码
//编译器会把下面这两个函数认为是同一个函数,编译报错,函数重复声明
void fun(int a);
void fun(const int a);

6.当一个形参类型有const或volatile修饰时,如果形参是按照指针或引用定义时,在识别函数声明是否相同时,就要考虑const和volatile修饰符.**(考虑CV特性),**那么又该如何考虑呢?

cpp 复制代码
#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++只有三种引用方式:左值引用(引用的是左值:可以取地址)、常性左值引用/万能引用(既可以引用左值:可以取地址,又可以引用右值:不可以取地址)、右值引用(引用的是右值:不可以取地址),当存在三个引用的函数时,我们应明确编译器的匹配顺序,根据实参的类型进行匹配!!!

对于左值,优先匹配左值引用,其次才是常性左值引用!

对于常性左值,直接匹配常性左值引用,不存在,编译器直接报错!

对于右值引用,优先匹配右值引用,其次才是常性左值引用!

  1. 对于普通的变量(它是左值),编译器首先优先匹配形参为左值引用的函数,如果不存在该函数,然后,匹配形参为常性左值引用(常引用)的函数,如果二者都不存在,编译器直接报错!
  2. 对于常变量(const修饰),编译器直接匹配形参为常性左值引用(常引用)的函数,如果该函数不存在,编译器直接报错(不能匹配参数为右值引用的函数,右值引用引用的是右值)!
  3. 对于右值,编译器首先优先匹配形参为右值引用的函数,如果该函数不存在,然后,匹配形参为常性左值引用(常引用)的函数,如果二者都不存在,编译器直接报错!
    7.注意函数调用的二义性; 如果在两个函数的参数表中,形参类型相同,而形参个数不同,形参默认值将会影响函数的重载.
cpp 复制代码
#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 函数重载解析的步骤

  1. 确定函数调用考虑的重载函数的集合,确定函数调用中实参表的属性(由形参表的属性确定调用那个函数)。
  2. 从重载函数集合中选择函数,该函数可以在(给出实参个数和类型)的情况下可以调用函数。
  3. 选择与调用最匹配的函数。

3.4 函数重载判断练习

cpp 复制代码
#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;
}
cpp 复制代码
#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
cpp 复制代码
#include<iostream>
using namespace std;

/*****下面两个函数是重载函数:参数的个数不同******/

 void func(int a);
 void func(int a,int b);

 int main()
 {
	 func(12);
	 func(12, 23);
 }
cpp 复制代码
#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++"函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者

原型时生成的字符串。修饰名由函数名、类名、调用约定、返回值类型、参数表共同决定。不同的调用方式,形成的修饰名也不同。

  1. **_stdcall调用:**Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。
  2. C调用(即用_cdecl 关键字说明):按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。
  3. _fastcall 调用:"人"如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字( DWORD )或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
  4. **thiscall调用:**仅仅应用于"C++ "类的成员函数。this 指针存放于ECX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

  1. 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们 可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
  2. 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
  3. 那么链接时,面对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++方式修饰函数名

cpp 复制代码
 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 后加一个标识符构成,在这里两个关键字的意义相同,它们表示后面的参数名代表一个潜在的内置或用户设计的类型。

cpp 复制代码
#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;
 }

编译阶段,编译器根据实参类型自动的生成如下函数代码:这也叫模板实参推演,其本质上还是函数重载,由编译器自动生成代码。

cpp 复制代码
 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;
 }

在编译过程中,根据函数模板的实参构造出独立的函数,称为模板函数。这个构造过程被称为模板实例化。

cpp 复制代码
#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 数组的推演及引用的推演

cpp 复制代码
#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 利用函数模板实现泛型编程

cpp 复制代码
下面这个打印数组的函数代码适用于一切类型的数组,我都可以打印整个数组,类型由编译器自动推演

#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 模板函数的重载与特化(完全特化、部分特化、泛化)

  1. 模板函数的重载是C++中允许通过模板实现多个函数版本的功能。在C++中,函数可以通过相同的名字但不同的参数列表来重载。模板函数也可以通过这种方式实现重载。
  2. 函数模板特化是指为特定类型提供一个专门的模板实现,与上面相比,而不是使用通用的模板版本。
cpp 复制代码
#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):用于按照线性顺序存储数据。

  1. std::vector:动态数组,支持快速随机访问和在末尾插入 / 删除元素。

  2. std::deque:双端队列,支持快速随机访问和在两端插入 / 删除元素。

  3. std::list:双向链表,支持在任何位置快速插入 / 删除元素。

  4. std::array:固定大小的数组,大小在编译时确定。

  5. std::forward_list:单向链表,支持在任何位置快速插入 / 删除元素,但只允许单向遍历。
    容器库的特点

  6. 泛型编程:通过模板实现,容器可以存储任意类型的对象。

  7. 自动管理内存:容器会自动管理其所需的内存,开发者无需手动分配和释放内存。

  8. 迭代器支持:所有容器都提供迭代器,用于遍历和操作元素。

  9. 算法兼容性:STL中的算法可以与容器无缝配合使用,提供诸如排序、搜索、复制等操作。

cpp 复制代码
使用类模板生成任意类型的顺序栈

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++中把作用域划分为:全局作用域,局部作用域、块作用域,名字空间作用域和类作用域。注意:作用域是针对编译器来说的,生存周期针对运行的时候来说的。函数被调用时,被调用函数内部的变量才会分配内存空间,当函数执行完毕,被调用函数内部的变量将会归还给操作系统。我们定义的变量存储在栈区或者堆区。

  1. 全局变量:函数之外定义的变量:存储在数据区,
  2. 局部变量:函数内部定义的变量:存储在栈区,
  3. 静态局部变量:函数内部定义的变量加static关键字修饰,只创建一次,保存上次的值,不会重新初始化:存储在数据区(字符串常量也是)
  4. 花括号内部的变量,只在花括号内部有效:存储在栈区。
  5. 类作用域是指在类定义内部的作用域,决定了类中的成员(包括成员变量和成员函数)的可见性和生命周期。
  6. 名字空间作用域:多文件编程时:多个源文件定义相同的全局变量名字或者函数名,在项目进行编译链接时候就会发生全局命名冲突!名字空间域是随标准C++而引入的。它相当于一个更加灵活的文件域(全局域)。

5.2 命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存 在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

名字空间域的引入,主要是为了解决全局名字空间污染(global namespace pollution)问题,即防止程序中的全局实体名与其他程序中的全局实体名的命名冲突。于是,便产生了命名空间。

5.3 命名空间的定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{} 中即为命名空间的成员。

cpp 复制代码
// 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 加命名空间名称及作用域限定符

cpp 复制代码
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将命名空间中某个成员引入

cpp 复制代码
   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名字空间名;

cpp 复制代码
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即可,这样就很方便。

  1. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对

象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模

大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间+
using std::cout展开常用的库对象/类型等方式。

这篇博客内容较为丰富,为后续学习好C++做好准备, 如果对此专栏感兴趣,点赞加关注!

相关推荐
XiaoLeisj18 分钟前
【JavaEE初阶 — 多线程】生产消费模型 & 阻塞队列
java·开发语言·java-ee
2401_8401922721 分钟前
python基础大杂烩
linux·开发语言·python
@东辰25 分钟前
【golang-技巧】- 定时任务 - cron
开发语言·golang·cron
机器人天才一号27 分钟前
C#从入门到放弃
开发语言·c#
Mr_Xuhhh2 小时前
递归搜索与回溯算法
c语言·开发语言·c++·算法·github
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
无敌岩雀2 小时前
C++设计模式行为模式———命令模式
c++·设计模式·命令模式
B20080116刘实2 小时前
CTF攻防世界小白刷题自学笔记13
开发语言·笔记·web安全·网络安全·php
Qter_Sean3 小时前
自己动手写Qt Creator插件
开发语言·qt
何曾参静谧3 小时前
「QT」文件类 之 QIODevice 输入输出设备类
开发语言·qt