C++ Primer Plus基础知识部分快速通关

发布时间 2023-04-01 23:53:03作者: 勤倦阁主

第二章

  • 在 C++中,\(main\)函数括号中使用void关键字表明拒绝任何参数,而空括号表示对是否接受参数保持沉默。
  • 连续赋值将从右向左进行。
  • 输出拼接长字符串可以如下书写:
    //当代码很长而追求风格的时候,这样操作。
cout << "1234567"
     << "7654321"
     << endl;
  • 类描述了一种数据类型的全部属性(包括可用其来执行的操作),对象是根据这些描述创建的实体。

第三章

  • 数据输入/常数可以以十进制、十六进制、二进制书写。
#include <iostream>
using namespace std;

int main()
{
    int a = 10, b = 0xf, c = 0b10100;
    cout << a << b << c << endl;
    return 0;
}
  • cout提供控制符以八进制、十进制与十六进制显示整数。
#include <iostream>
using namespace std;
int main()
{
    using namespace std;
    int chest = 42;
    int waist = 42;
    int inseam = 42;
    cout << "Monsieur cuts a striking figure !" <<endl;
    //所有设置,在更改之前将会一直延续
    //默认显示十进制
    cout << "chest = " << chest << "(decimal for 42 ) " << endl;
    cout << hex; // 显示十六进制
    cout << "'waist = " << waist <<"(hexadecimal for 42)" << endl;
    cout << oct; // 十进制
    cout << "inseam = " << inseam <<: " (octal for 42 ) " << endl;
    return 0 ;
}
  • 通用字符名

ISO 10646,Unicode 与 UTF-8 的那些事

  • 常量定义,建议使用const限定而非#define,且必须在定义时赋值,否则即乱码数字,后续无法修改。
  • 浮点数赋值直接赋,不认识后缀 f
  • auto声明,自动根据初始值类型推断变量类型。

第四章

数组

  • 数组创建,数组大小必须为常量,不能是变量。
    • 数组是一种复合类型,是依托于其他类型来创建的(C 称之为派生,而 C++对类关系使用派生这一术语)
  • 数组初始化,如果在定义时仅对一部分元素初始化,则其他元素将被设置为 0

字符串

  • 拼接字符串常量。字符串过长时允许拼接字符串字面值。
    • 任两个由空白分隔符(空格、\t\n)分割的字符串常量都会被自动拼接为一个。
  • getgetline,二者使用方式基本相同,只是前者继承 cin 的特性,不会读取换行符后再从字符串中删除,而是把换行符留在输入队列中。
/*在没有任何参数的情况下
 *cin.get()会读取下一个字符,即使是换行符
 *因此可用以清空*/
cin.get(name , ArSize);// read first line
cin.get();// read newline
cin.get(dessert. Arsize ) ;// read second line
//前两行可以合并为:
//cin.get(name, ArSize).het()
  • 其他类型字符的使用
    • wchar_t
    • char16_t
    • char32_t
  • C++string提供字符串翻转函数。

结构体

  • 结构体初始化
    • 按声明顺序将列表声明为结构体初始化
    • 指定对应初始化
    • 构造函数初始化
#include <iostream>
using namespace std;

struct student
{
    int id;
    string name;
    short scores[3];
    void student(int id,string name,short scores[])
    {
        this->id=id;
        this->name=name;
        for(char i=0;i<3;i++)
        {
            this->scores[i]=scores[i];
        }
    }
};

int main()
{
    student a={2021110884,"QJGZ",150,150,150};
    strdent b={.id=2021110884,.name="陈一豪",.scores={150,150,150}};
    student c(2021110884,"QJGZ",{150,150,150});

    return 0;
}

注意,前两种方法不能使用在有构造函数的结构体中,否则会报错“指示符不能用于非聚合类型”。

对于所有非聚合类型,不能使用初始化列表(即用列表形式初始化,前两种方法是也)。

聚合类型定义如下:

  • 数组
  • 不包含(构造函数、private 和 protect、基类、虚函数)的类、结构体和联合体

共用体

举例:一个小商品目录,一些商品 id 为字符串,另一些为数字:

union id
{
    long id_num;
    char id_str[32];
};
struct good
{
    char brand[20];
    int type;
    id id_val;
};

同时,有匿名共用体:


struct good
{
    char brand[20];
    int type;
    union
    {
        long id_num;
        char id_str[32];
    }
};

匿名共用体没有自己的名称,成员将成为位于同一位置的不同变量,被视为上一级的成员。

枚举

啥 JB 东西,看不懂!!!

指针

  • int*被理解为一种复合类型而存在,但int* a, b被解释为一个指针和一个整型

哪个 TM 大聪明的主意?

指针、数组与指针算术

指针与数组基本等价的原因在于指针算数和 C++内部处理方式。

算术:

  • 整型变量递增+1,指针变量递增加的是指向的类型的字节数。这同时表明,C++将数组名解释为地址。
  • 可以以相同的形式使用指针与数组名。
    • 方括号数组表示
    • 加偏移量而后解引用
  • 注意:
    • 数组名直接取地址为整个数组作为内存块的地址,递增时加的是整块大小
    • 数组名本身是首个元素的地址,递增时加的是元素大小
    • 二者初值相同

变量存储方式

  • 局部存储
  • 静态存储
    • 定义为全局变量
    • 使用static关键字,如static int c=3;
  • 动态存储

数组替代

  • vector
    • 分配在自由存储区或堆中。
    • 功能强大,效率略低
  • array
    • 长度固定如数组,和数组一样分配在栈上而非自由存储区,因而效率同于数组,但更方便安全。
#include <array>
using namespace std;

int main()
{
    array<int,5> a;
    int i;
    for(i=0;i<5;i++)
    {
        cin>>a[i];
    }
    for(i=0;i<5;i++)
    {
        cout<<a[i];
    }
    return 0;
}

第五章

递增递减运算符

  • 写在后边,则变量先使用,再加/减
  • 写在前边,则变量先加/减,再使用
  • 避免使用不完整的表达式
    //完整的表达式
    i=0
    while(i++<10)
    {
        cout<<i<<endl;
    }
    /*在上例中,先执行判断,而后递增,再输出,因而输出结果为[1,10]
     *这是因为i++<10本身是一个完整表达式
     */

    //不完整表达式
    y=(4+x++)+(6+x++);
    /*上式中,(4+x++)并不是一个完整表达式,无法保证执行完本处计算后x即刻+1
     *因而整条语句结果并不确定
     */

解释:

  • 副作用指计算表达式时对某些东西进行了修改
  • 顺序点指程序过程中一个点,在这里将在进入下一步之前对所有副作用进行评估。

在 C++中,语句的分号是一个顺序点,在下一步之前会把所有副作用造成的修改弄到位。
上例中,前者即有顺序点,执行之前就完成了副作用;后者顺序点在分号,因而副作用结果并不确定。

本身即为运算表达式,有返回值,返回值即上所谓“使用”。

指针与递增递减

递增递减作用不赘。

前缀递增递减与解引用运算优先级相同,因而从内向外解释:

    int a[2]={1,2},*pt=a;
    cout<<*++pt<<*(++pt)<<endl;
    //输出结果,二者相等,即*++pt先执行递增,而后经过顺序点,再解引用,实际为a[1]=2

后缀递增递减优先级高于解引用,因而*pt++结果与上亦同。

逗号运算符

逗号运算符允许把两个表达式放一行,表达式的值是后半句的值。

普通赋值运算式的值为右值。

#include<iostream>
using namespace std;
int main()
{
    int a;
    a=1,2;
    cout<<a<<endl;//输出1,2不起作用
    a=(1,2);
    cout<<a<<endl;//输出2
    return 0;
}

循环

for循环与while循环的本质是相同的:

for(init-expression; test-expression; update-expression)
{
    statements
}

//等价于下述表达
init-expression;
while (test-expression)
{
    statements;
    update-expression;
}

区别:

  • for循环中省略测试表达式时默认为 true,而while中禁止此种行为
  • 在循环体中有continue语句时,二者表现不再完全等价,稍有不同

循环与文本输入

  • 逐个读取字符需要检查遇到的每个字符包括空格、制表、换行等非显示字符。此时应当使用cin.get()函数。
  • 函数cin.get(ch)读取输入中的下一个字符(包括空格)并赋值给 ch,可以替换cin>>ch

文件尾(EOF)条件

重要性

  • 很多操作系统(包括 Unix、Linux 和 Windows 命令提示符模式)都支持重定向,允许用文件替换键盘输入。

例如,假设在 Windows 中有一个名为 gofish.exe 的可执行程序和一个名为 fishtale 的文本文件,则可以在命令提示符模式下输入下面的命令:

gofish < fishtale

来实现以文本文件代替输入。

  • 很多操作系统都允许通过键盘来模拟文件尾条件。
    • 在 Unix 中,可以在行首按下 Ctrl+D 来实现;
    • 在 Windows 命令提示符模式下,可以在任意位置放 Ctrl+Z 和 Enter。
    • 有些 C++实现支持类似的行为,即使底层操作系统并不支持。

键盘输入的 EOF 概念实际上是命令行环境遗留边倒下来的。

然而:

  • 用于 Mac 的 Symantec C++模拟了 Unix,将 Ctrl+D 视为仿真的 EOF
  • Metrowerks Codewarrior 能够在 Macintosh 和 Windows 环境下识别 Ctr1+Z
  • 用于 PC 的 Microsoft Visual C++、Borland C++ 5.5 和 GNU C++都能够识别行首的 Ctrl+Z,但用户必须随后按下回车键。

总之,很多 PC 编程环绕都将 CtrI+Z 视为模拟的 EOF,但具体细节(必须在行首还是可以在任何位置,是否必须按下回车键等)各不相同。

  • 如果编程环境能识别 EOF,则既可以使用重定向文件输入也可以用键盘,岂不美哉?

实现

检测到 EOF 后,cin 将两位(eofbitfailbit)均设置为 1。(可通过成员函数bool cin.eof()bool cin.fail()查看情况,且是在读取之后查看。)

第六章

逻辑运算符相关

  • 逻辑与/或的优先级均低于关系运算符
    • 逻辑与的优先级高于逻辑或
  • 逻辑非的优先级高于所有关系运算符和算术运算符,因而要对表达式求反必须括起来。
  • 另一套表示方式
运算符 另一种表示
&& and
|| or
! not

字符函数库

cctype(C 语言中的 ctype.h)可以用来确定字符是否为大/小写字母、数字、标点等。其优点在于更简单、更通用(字母、数字之类在不同的编码方式之下不一定都像 ASCII 中一样的连续分布)。包含的函数如下:

namespace std {
    int isalnum(int c);
    int isalpha(int c);
    int isblank(int c);
    int iscntrl(int c);
    int isdigit(int c);
    int isgraph(int c);
    int islower(int c);
    int isprint(int c);
    int ispunct(int c);
    int isspace(int c);
    int isupper(int c);
    int isxdigit(int c);
    int tolower(int c);
    int toupper(int c);
}

第七章

基础知识

  • 函数原型:说明函数名、形参类型/名称、返回值类型
    • 其实就是\(main()\)之前的函数声明
    • 函数原型中的函数特征标(参数列表)可以省略标识符(形参名称)而只是指出其类型

其实就是那个二锅头,兑的那个白开水

  • 函数定义:函数的本体实现

函数与数组

//函数定义
int sum_arr(int arr[], int n);//对数组中元素求和

//函数调用
int a[10]={……};//一堆破数
int sum=sum_arr(a,10);

以上定义的函数,第一个形参是指针而非数组,但可以当做数组使用。

原因:C++与 C 一样,将数组名视为指针,指向第一个元素地址,对应元素大小为单个元素大小,详前。(相比之下,&a虽然也是指向首地址,但大小是整块数组的大小。)因而第一个形参实际是int* arr,即定义应当为:

int sum_arr(int* arr, int n);

这证明两种形参声明同时正确。在 C++中,int* arrint arr[]当且仅当出现在函数头或函数原型的时候,含义才相同。它们都意味着 arr 是一个int*

使用数组区间的函数

屁话一堆,无非就是给定首尾元素的指针为形参,在中间作妖罢了。不说也罢。

指针与 const

将 const 用于指针

  • 指针指向\(const\)常量,防止修改
  • 传参将指针本身声明为常量,防止修改指针指向的位置,但可以修改内容

优点:

  • 避免无意见修改数据导致错误
  • const可使函数能处理 const 与非 const 实参,否则只能接收后者。
//只能防止修改pt指向的值,但pt本身可以改成新地址
int age=39;
const int* pt=&age;
int sage=80;
pt=&sage;//是可以的,不会出问题

//下面两个是有区别滴,仔细看
int sloth=3;
const int* ps=&sloth;//指向const int类型的指针
int* const finger=&sloth;//指向int的const指针
//前者如上例,不能修改内容,但能重新指向
//后者则不能重新指向,但可以修改内容

二维数组作参数

int sum(int (*arr)[4], int size);
//等价于以下
int aum(int arr[][4], int size);

//不能声明为
int sum(int *arr[4], int size);

之所以不能声明为最后一个,是由于第一个形参应当是指向数组的指针,而不是单纯的二重指针。上述两个可用原型都指出,第一个形参类型实际上是指向 4 个元素的数组的指针。我之困惑于此也久矣,今乃得闻。

基于此,则传参之后仍可以二维数组待之,其原因为arr[i]被解释为*(arr+i),是第 i 个数组,则arr[i][j]被解释为第 i 个数组的第 j 个元素,即为二维数组。

返回 C 风格字符串

听起来很扯淡,对吧?返回值怎么可能是数组类型?内部声明的数组在退出的时候不是就释放了吗?

那么,有没有一种可能,我是说可能,内部用new申请一段空间给char*,然后写完了返回,在主函数中用完了在delete呢?

函数指针

为了实现函数指针,必须要完成:

  • 获取函数的首地址
    • 方法很简单,直接使用函数名即可。

think()是一个函数,则think就是该函数地址。要作为参数传递,必须传递函数名。要区分传递的是函数名还是函数返回值哦!

  • 声明一个函数指针
    • 声明一般指针需要说明指向的类型,函数指针也一样。这就是说,声明应指出函数的返回类型及其特征标(参数列表)。
  • 使用函数指针来调用函数

例程如下:

#include <iostream>
#include <string>
using namespace std;

bool hello(string name)
{
    cout << "Hello, " << name << endl;
    return true;
}

int main()
{
    // 函数指针
    bool (*pf)(string);
    // 函数参数为string型,返回值bool,指针名称pf。

    /* 上例是将<kbd>(*pf)</kbd>替代了<kbd>pam</kbd>,也就是说
     * <kbd>(*pf)</kbd>是函数
     *从而<kbd>pf</kbd>是函数指针。
     */

    /* 为提供正确的运算符优先级,应当用括号将`*`与`pf`括起来
     * 括号优先级高于`*`,从而:
     * <kbd>*pf(string)</kbd>意味着<kbd>*pf(string)</kbd>是返回指针的函数
     * <kbd>(*pf)(string)</kbd>意味着<kbd>pf</kbd>是指向函数的指针
     */

    // 赋值
    pf = hello;

    // 调用
    cout << pf("World") << endl;
}

我又要骂人了,这什么东西,类型声明这么复杂?不用了不用了,享受不来。

没事,C++11 还有个特性叫auto,不是么?

上边的例程可以被改写成如下形式:

#include <iostream>
#include <string>
using namespace std;

bool hello(string name)
{
    cout << "Hello, " << name << endl;
    return true;
}

int main()
{
    //函数指针
    auto pf = hello;
    // 调用
    cout << pf("World") << endl;
}

这自动推断类型,多是一件美逝了!让我们一起说,谢谢auto

另外,如果有若干函数返回类型和特征标都相同,都需要调用的话,何不考虑一下函数指针数组呢?更进一步地,为什么不选择创建一个指向整个函数指针数组的指针呢?

#include <iostream>
using namespace std;

// 没错,函数原型可以不写形参名称的
// 假设这段代码之外我们有函数的具体定义
const double *f1(int);
const double *f2(int);
const double *f3(int);

int main()
{
    // 定义一个指向函数的指针
    const double *(*p[3])(int) = {f1, f2, f3};
    // 定义一个指向指针的指针
    auto pp = &p;

    // 直接调用函数指针数组
    cout << p[0](1) << endl;
    cout << p[1](2) << endl;
    cout << p[2](3) << endl;

    // 调用指向指针数组的指针
    cout << (*pp)[0](1) << endl;
    cout << (*pp)[1](2) << endl;
    cout << (*pp)[2](3) << endl;
    return 0;
}

const double *f1(int i)
{
    static double d = 1.1;
    return &d;
}

const double *f2(int i)
{
    static double d = 2.2;
    return &d;
}

const double *f3(int i)
{
    static double d = 3.3;
    return &d;
}

第八章

内联函数

内联函数是 C++为提高程序运行速度而做的一项改进。常规函数与内联函数区别不在于编写方式,而在于 C++编译器如何将它们组合到程序中。要了解它们的区别,必须深入到程序内部。

编译过程的最终产品是可执行程序,由一组机器语言指令组成。运行程序时,操作系统将指令载入内存,因而每条指令都有其特定的内存地址。计算机随后一一调用执行,或者有时候向前/向后跳转到特定地址,如循环、条件判断、分支语句等。

常规函数调用也是一个跳转,在此过程中,程序首先将跳转后需要执行的指令的地址压栈,并将现在的寄存器中参数复制到堆栈(为此保留的内存区),跳转到目标函数的地址,执行该函数,在遇到返回指令时,返回地址出栈,程序跳转到该地址,然后将参数复制回来,从而使原来的函数继续进行。

而内联函数的编译代码是与其他程序的代码“内联”的,换言之,编译器将使用内联函数的代码替换掉此处的函数调用,直接 copy 进来了。由于缺少了复制和跳转的过程,因而执行速度较快,但同时占据更多内存,调用几次该函数,该函数代码就被抄写几份。使用过程中需要综合考虑是否使用内联函数。

要使用内联函数,必须采取下列措施之一:

  • 在函数原型前加关键字inline
  • 在函数定义前加关键字inline

程序员做出内联请求时,编译器不一定同意(p.s.到底是我写代码还是编译器写代码?气抖冷!),它可能认为函数过大或者注意到有递归(众所周知,内敛这个形式不能递归的),或者有些编译器没有启用或实现内联这一功能。

#include <iostream>
using namespace std;

// 这是一个内联函数
inline double square(double x)
{
    return x * x;
}

int main()
{
    double a, b;
    double c = 13.0;

    a = square(5.0);
    b = square(4.5 + 7.5); // 可以传递表达式

    cout << "a = " << a << ", b = " << b << endl;
    cout << "c = " << c;
    cout << ", c squared = " << square(c++) << endl;
    cout << "Now c = " << c << endl;
    return 0;
}

输出结果为:

a = 25, b = 144

c = 13, c squared = 169

Now c = 14

输出表明,内联函数与普通函数一样,按值传递参数,参数为表达式则传递表达式的值。

C 语言中也可以像这样,不写函数原型,直接以函数定义充当函数原型。

内联的原始实现:C 中的#define

#include <stdio.h>
#define square(x) x*x

int main()
{
    double a, b, c = 3, d;
    a = square(5.0);
    b = square(4 + 5);
    d = square(c++);
    printf("a = %lf\nb = %lf\nd = %lf\n", a, b, d);
    return 0;
}

这并不是通过传递参数实现,而是通过文本替换实现的——\(x\)是“参数”的符号标记。

上例只有\(a\)输出正确,可以用括号进行改进:

#define square(x) ((x) * (x))

即使如此,后两者依然输出错误,即无法实现按值传递。所以,使用内联函数应当尽可能考虑使用 C++的内联,而不是 C 的宏。

引用变量

引用变量是什么?能吃吗?

引用变量是已定义变量的一个别名。话不多说,上例程。

#include <iostream>
using namespace std;
int main()
{
    int n = 0;
    int &r = n;
    r = 1;
    cout << "n = " << n << endl;
    cout << "Addr r is " << &r << endl;
    cout << "Addr n is " << &n << endl;
    return 0;
}

运行结果如下:

n = 1
Addr r is 0x7ffce76ff48c
Addr n is 0x7ffce76ff48c

由结果可知,r 只是 n 的一个别名,在修改 r 的时候实质上就是在修改 n。二者的地址是相同的。

请注意:

  • 例程第 6 行的&并非取地址符,而是将 r 声明为一个int&型的变量;而在第 9/10 行的&则是取地址符。
  • 引用变量必须在声明的时候进行初始化,一旦初始化,即宣誓效忠,至死不渝,是无法改变的。换言之,int &r = nint* const *pr = n的一个封装表示而已。

引用变量作函数参数

在 C/C++一般的函数调用中,参数都是按值传递,即 copy 一份送到调用函数里使,即使做出了更改也不会直接影响原函数里的值,除非用返回值将其返回。有时候一个函数本来有自己需要返回的东西,又想把这个量修改了,就不好办了(用pair或者结构体,多少有点麻烦,还要为此创建一个类型)。这时候就可以考虑引用变量。

C-风格字符串作 string 对象引用参数

#include <iostream>
using namespace std;

const string &fuck(string &s1, const string &s2)
{
    s1 = s2 + s1 + s2;
    return s1;
}
int main()
{
    string s1 = "Hello";
    cout << fuck(s1, ", World!\n") << endl;
    return 0;
}

程序可以接受将 C-风格字符串赋值给string&

首先,string类定义了char*string的转换,因而可以使用字符数组初始化字符串;

其次,类型为 const 引用的形参有一个属性:在形参与实参不匹配但可以转换的时候,程序会创建临时变量进行匹配,然后传参过去。换言之,程序调用之前创建一个临时string,把引用传递过去。

对象、继承和引用

使得能将特性从一个类传递给另一个类的语言特性称为继承。继承的另一个特性是,基类引用可以指向派生类对象而无需进行强制转换。这种特性的实际结果为,派生类中可以定义一个以基类引用为参数的函数,调用该函数时,既能以基类为参数,也可以派生类为参数。

ofstreamostream为例,前者为派生类,后者为基类,因为前者建立在后者基础之上。派生类继承了基类的方法,这意味着ofstream可以使用ostream的特性,如格式化方法和输出运算符;参数类型为ostream&的函数既可以接收ostream对象(如 cout),也可以接收已经声明的ofstream对象为参数。

何时使用引用

使用引用参数的原因:

  • 能修改调用函数的数据对象
  • 通过传递引用而非整个数据对象,提高运行速度。

当数据对象较大(如结构体、类对象),第二个因素占主要,这也是使用指针的原因,因为引用实际上是指针的另一个接口。

什么时候使用指针、什么时候使用按值传递呢?
对于使用传递的值而不作修改的:

  • 数据量小,按值传递
  • 是数组,指针传递,这是唯一办法,并将指针声明为指向 const 的指针
  • 数据对象是较大的结构体,使用 const 引用或 const 指针
  • 是类对象,使用 const 引用

对于修改调用函数数据的函数:

  • 修改的是内置数据类型,使用指针
  • 是数组,就只能用指针
  • 是结构体,用指针或引用
  • 是类对象,用引用

函数多态

函数多态是 C++在 C 基础上增加的功能。

  • 默认参数允许以不同数目的参数调用同一函数
  • 重载允许使用多个同名的函数。

默认参数

默认参数指函数调用中省略实参时自动使用的实参值。

// 一个使用默认参数的例子
#include <iostream>
using namespace std;
int add(int a, int b = 1, int c = 2)
{
    return a + b + c;
}
int main()
{
    cout << add(5) << endl;       // 8
    cout << add(5, 6) << endl;    // 13
    cout << add(5, 6, 7) << endl; // 18
    return 0;
}

对于带参数列表(有形参)的函数,必须从右向左添加默认值,即所有有默认值的形参必须在所有无默认值形参的右边。使用时,传递的实参被从左到右依次赋给形参,而不得跳过任何形参。

函数重载

函数重载指可以有多个同名函数,它们以参数列表(形参,不包括返回值类型)为区别。C++使用上下文来确定要使用的重载函数版本。

//一个函数重载示例程序
#include <iostream>
using namespace std;

int add(int a, int b)
{
    return a + b;
}

double add(double a, double b)
{
    return a + b;
}

int main()
{
    cout << add(1, 2) << endl;
    cout << add(1.1, 2.2) << endl;
    return 0;
}

仅当在函数执行基本相同的任务而使用不同的数据类型时,才适合重载

重载虽好,可不要贪杯哦~

函数模板

函数模板是通用的函数描述,使用泛型来定义函数,其中的泛型可用于指定的具体类型(即int/double等)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。

少扯淡,一个代码就明白了

//一个使用函数模板的历次
#include <iostream>
using namespace std;

template <typename T>
T max5(T a[])
{
    T max = a[0];
    for (int i = 1; i < 5; i++)
        if (a[i] > max)
            max = a[i];
    return max;
}

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    double b[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
    cout << max5(a) << endl;
    cout << max5(b) << endl;
    return 0;
}

第一行指出,要建立一个模板,类型命名为 T。关键字templatetypename是必须的,但可以使用关键字class替代typename>。另外,必须使用尖括号。类型名可以随意写。

函数模板本身并不创建任何函数,只是告知编译器如何定义函数,由编译器探明和处理,各自按照模板新建一个函数。因而,函数模板不能缩短可执行程序,在上例中最终仍然是两个独立函数,并无函数模板。但模板的好处在于使生成多个同类型函数更简单可靠。

常见用法为将模板放在头文件中,在需要使用的地方包含头文件

模板重载

模板满足了对不同类型使用同一种算法函数的需求,但并非所有类型都是用完全相同的算法。为满足此种需求,可以像重载常规函数一样重载模板定义,仍然需要满足参数列表不同便是了。

template<class T>//一个模板
void swap(T &a,T &b);

template<class T>//另一个模板
void swap(T* a,T* b,int n);

模板的局限性

模板局限性在于很可能无法处理某些数据类型。如赋值,一般类型赋值直接赋值即可,但数组则不可以;比较大小,一般数据类型可以直接比较,结构体则不行。

具体化与实例化

又是什么 JB 东西,似乎有用但似乎用处不大,先放着吧。

编译器选择哪个函数版本

对于函数重载、函数模板和函数模板重载,C++需要一个定义良好的解析策略来确认调用哪一个函数定义,尤其是有多个参数的时候。此过程称为重载解析。

  • 创建候选函数列表,包含所有与被调用函数同名的函数与模板
  • 使用候选函数列表,根据实参来创建可行函数列表
  • 确定是否有最佳的可行函数,有则使用,无则报错。优先级如下:
    • 完全匹配,但常规函数优先于模板
    • 提升转换(char/chort-->intfloat-->double
    • 标准转换(int-->charlong-->double
    • 用户定义的转换,如类声明中定义的转换。

完全匹配时,允许某些无关紧要的转换,如下表。

实参 形参
Type Type&
Type& Type
Type[] *Type
Type(argument0list) Type(*)(argument-list)
Type const Type
Type volatile Type
Type* const Type
Type* volatile Type
//调用函数
may('B');

//所有同名
void may(int);               // 1
float may(float, float = 3); // 2
void may(char);              // 3
char *may(const char *);     // 4
char may(const char &);      // 5

template <class T>
void may(const T &);         // 6

template <class T>
void may(T *);               // 7

由上述规则可以看出,优先级情况如下:

  • 完全匹配:3、5、6
  • 提升转换:1
  • 标准转换:2
  • 不符合:4、7

其中,3、5 是常规函数,优先级高于 6 模板。

有两个最佳选择的情况一般认为是错误的,但有特例。

  • 指向非 const 数据的指针和引用优先与非 const 指针和引用参数匹配,反之然
  • 其中一个是非模板函数而另一个不是,此时废模板函数优先于模板函数(包括显式具体化)