C++基础学习

文章目录

  • 八 函数
    • 8.8 函数类型推导
      • 一:关于auto
      • 二:关于尾随返回类型语法
      • 三:类型推导不能用于函数参数类型
    • 8.9 函数重载介绍
      • 一:函数重载简介
      • 二:重载决议
    • 8.10 函数重载微分
      • 一:如何区分重载函数
    • 8.11 重载解析
      • 一:解决重载的函数调用
      • 二:参数匹配序列
      • 三:暧昧匹配(不明确匹配)
    • 8.13 函数模板
      • 一:函数模板
    • 8.14 函数模板实例化
      • 一:关于函数推导:
      • 二:通用编程
        • 三:本节学习结论
    • 8.15 具有多种模板类型的函数模板
      • 一:三种函数模板
      • 二:缩写功能模板
    • 8.16 函数总结
      • 一:总结
  • 九 复合类型
    • 9.1 枚举
      • 一:命名枚举和枚举器
      • 二:枚举器的作用域
      • 三:枚举值
      • 四:枚举类型评估和输入/输出
      • 五:打印枚举器
      • 六:枚举分配和向前声明
      • 七:随堂测
    • 9.2 枚举类
    • 9.3 结构
      • 一:声明和定义结构
      • 二:访问结构体成员
      • 三:初始化结构
      • 四:非静态成员初始化
      • 五:分配给结构
      • 六:结构和函数
      • 七:嵌套结构
      • 八:结构体大小和数据结构对齐
      • 九:跨多个文件访问结构
    • 9.4 随机数的生成
      • 一:在C++中生成随机数
      • 二:PRNG序列和播种
      • 三:在任意值之间生成随机数
      • 四:什么是一个好的PRNG?
      • 五:std::rand()是一个普通的PRNG
      • 六:使用Mersenne Twister更好的随机数
      • 七:使用随机库
  • 十 数组
    • 10.1 数组概念
      • 一:数组下标
      • 二:数组数据类型
      • 三:数组下标
      • 四:固定数组声明
      • 五:关于动态数组的注释
    • 10.3 数组和循环
      • 一:循环和数组
      • 二:混合循环和数组
    • 10.4 使用选择排序对数组进行排序
      • 一:排序工作原理
      • 二:选择排序
    • 10.5 多维数组
      • 一:初始化二维数组
      • 二:访问二维数组中的元素
      • 三:二维数组示例
    • 10.6 C风格的字符串
      • 一:C风格的字符串
      • 二:C风格的字符串和std::cin
    • 10.7 std::string_view介绍
      • 二:介绍std::string_view
      • 三:查看修改功能(string_view)
      • 四:std::string_view使用于非空终止字符串
      • 五:所有权问题
      • 六:将std::string_view转换为std::string
      • 七:通过data()函数打开窗口
    • 10.8 指针介绍
      • 一:地址运算符(&)
      • 二:简介运算符
      • 三:address-of运算符返回一个指针
      • 四:通过无效指针进行间接访问的警告
      • 四:指针大小
    • 10.9 空指针
      • 一:空值和空指针
      • 二:通过空指针间接
      • 三:对于空指针使用0(或者NULL)的危险
      • 四:C++11中的nullptr
    • 10.10 指针和数组
      • 一:整列衰减
      • 二:指针和固定数组的区别
      • 三:重新审视将固定数组传递给函数
      • 结构体和类中的数组不会衰减
    • 10.10 指针和数组
      • 一:整列衰减
      • 二:指针和固定数组的区别
      • 三:重新审视将固定数组传递给函数
      • 结构体和类中的数组不会衰减
    • 10.11 指针算术和数组索引
      • 一: 指针运算
      • 二:数组在内存中按照顺序排序
      • 三:指针运算、数组和索引背后的奇妙
      • 四:使用指针遍历数组
    • 10.12 使用new和delete动态分配内存
      • 一:动态分配单个变量
      • 二: 初始化动态分配的变量
    • 10.13 动态分配数组
      • 一:动态删除数组
      • 二:动态数组几乎与固定数组相同
      • 三:初始化动态分配的数组
      • 四:调整数组大小
    • 10.14 指针和常量
      • 一:指向常量值的指针
      • 二:常量指针
    • 10.15 引用和常量
      • 一:初始化对const值的引用
      • 二:对左值的引用延长了引用值的生命周期
      • 三:作为函数参数的常量引用
    • 10.16 使用指针和引用选择成员
    • 10.17 For-each循环
      • 一:For-each循环
      • 二:For-each循环和引用
      • 三:使用for-each循环重写最高分示例
      • 四:我们可以获取当前元素的索引吗?
    • 10.18 指向指针和动态多维数组的指针
      • 一:指向指针的指针
      • 二:指针数组
      • 三:二维动态分配数组
      • 四:通过地址传递指针
    • 10.19 std::array介绍
      • 一:array介绍
      • 二:大小和排序
      • 三:将不同长度的std::array传递给函数
      • 四:通过size_type手动摸索std::array
    • 10.20 std::vector简介
      • 一:std::vector简介
      • 二:自动清理防止内存泄漏
      • 三:向量记住它们的长度
      • 四:如何调整矢量的大小
      • 五:压缩布尔值
    • 10.21 迭代器介绍
      • 一:迭代器介绍
      • 二:指针作为迭代器
      • 三:标准库迭代器
      • 四:回到基于范围的for循环
      • 五:迭代器失效(悬空迭代器)
    • 10.22 标准库算法介绍
      • 一:使用std::find按值查找元素
      • 二:使用std::find_if查找匹配某个条件的元素
      • 三:使用std::count和std::count_if来计算出现的次数
      • 五:使用std::sort自定义排序
      • 六:使用std::for_each对容器的所有元素进行排序
  • 十一 函数
    • 11.1 函数参数和参数
      • 一:什么是按值传递
    • 11.2 按值传递参数
      • 二:按值传递的优缺点
    • 11.3 通过引用传递参数
      • 一:通过引用传递
      • 二:通过输出参数返回多个值
      • 三:通过引用传递的限制
      • 四:通过常量引用传递
      • 五:对指针的引用
      • 六:引用传递的优缺点
  • 十二 面向对象编程
    • 1.面向对象基本要素
      • 1 class
      • 2 成员函数
      • 3 会员类型
      • 4 关于C++结构体的注释
    • 2 公共与私有符访问说明符
      • 1 公共和私人成员
      • 2访问说明符
      • 3 混合访问说明符
      • 4 访问控制在每个班级的基础上工作

八 函数

8.8 函数类型推导

一:关于auto

由于编译器已经必须从 return 语句中推导出返回类型,因此在 C++14 中,该auto关键字被扩展为进行函数返回类型推导。这是通过使用auto关键字代替函数的返回类型来实现的。

auto add(int x, int y)
{
   
    return x + y;
}

因为 return 语句是返回一个int值,所以编译器会推断出这个函数的返回类型是int。使用auto返回类型时,所有返回值的类型必须相同,否则会导致错误。例如:

auto someFcn(bool b)
{
   
    if (b)
        return 5; // return type int
    else
        return 6.7; // return type double
}

注意: 使用auto返回类型的函数的一个主要缺点是这些函数在使用之前必须完全定义(前向声明是不够的)。

#include 
auto foo();
int main()
{
   
    std::cout << foo(); // the compiler has only seen a forward declaration at this point
    return 0;
}
auto foo()
{
   
    return 5;
}
//错误 C3779:“foo”:在定义之前不能使用返回“auto”的函数。

直接将函数定义放在主函数之前不会出现报错。
原因:向前声明没有足够的信息提供给编译器推断函数的返回类型,这意味着返回的普通函数auto通常只能从定义它们的文件中调用。

二:关于尾随返回类型语法

该auto关键字还可用于使用尾随返回语法声明函数,其中返回类型在函数原型的其余部分之后指定。
参考以下函数

int add(int x, int y)
{
   
  return (x + y);
}

//使用尾随语法等效
auto add(int x, int y) -> int
{
   
  return (x + y);
}
//在这种情况下,auto不执行类型推导——它只是使用尾随返回类型的语法的一部分。

那我们为什么要使用这种语法结构呢?

  1. 函数名称都排列起来算不算一种好事呢!
  2. C++中的一些高级语法也需要尾随返回语法,如lambdas(匿名函数,在11.13中有所介绍)。
  3. 常规依旧使用传统的语法模式,除非需要此类语法的时候。

三:类型推导不能用于函数参数类型

#include 
void addAndPrint(auto x, auto y)
{
   
    std::cout << x + y;
}
int main()
{
   
    addAndPrint(2, 3); // case 1: call addAndPrint with int parameters
    addAndPrint(4.5, 6.7); // case 2: call addAndPrint with double parameters
}

类型推导对于函数参数并不适用,并且在C++20之前上述程序无法编译成功(函数参数无法具有自动类型的错误)
而在C++20中,auto关键字被拓展,以便于程序能运行成功,但是auto在此类情况下不会调用类型推导,而是会触发一个function templates,此功能在实际处理此类情况。
关于function templates:链接:
模板(Templates)使得我们可以生成通用的函数,这些函数能够接受任意数据类型的参数,可返回任意类型的值,而不需要对所有可能的数据类型进行函数重载。这在一定程度上实现了宏(macro)的作用。它们的原型定义可以是下面两种中的任何一个:

template function_declaration;
template function_declaration;

8.9 函数重载介绍

一:函数重载简介

函数重载允许我们创建多个同名函数,只要每个同名的函数都有不同的参数(或者是参数可以其他方式区分),名称共享的函数称为重载函数。
我们现在add()在同一范围内有两个版本:

int add(int x, int y) // integer version
{
   
    return x + y;
}

double add(double x, double y) // floating point version
{
   
    return x + y;
}

int main()
{
   
    return 0;
}

本质:要使编译器可以区分每个重载函数,就可以重载函数,如果无法区分,则会导致编译错误。

相关补充:C++中的运算符只是函数,所以运算符也可以重载(13.1运算符重载中可以讨论这一点)

二:重载决议

1.当对已重载的函数进行函数调用时,编译器将尝试根据函数调用中使用的参数将函数调用与适当的重载相匹配。这称为重载决议。

#include 
int add(int x, int y)
{
   
    return x + y;
}
double add(double x, double y)
{
   
    return x + y;
}
int main()
{
   
    std::cout << add(1, 2); // calls add(int, int)
    std::cout << '\n';
    std::cout << add(1.2, 3.4); // calls add(double, double)
    return 0;
}
//编译结果  3 4.6

2.编译
为了编译使用重载函数的程序,有两件事必须是正确的:

  1. 每个重载函数都必须与其他函数区分开来。我们将在第8.10课- 函数重载微分中讨论如何对函数进行微分。
  2. 对重载函数的每次调用都必须解析为重载函数。我们将在第8.11课- 函数重载解析和模糊匹配中讨论编译器如何将函数调用匹配到重载函数。

8.10 函数重载微分

一:如何区分重载函数

区分重载函数的最简单方法是确保每个重载函数具有不同的参数集(数量和/或类型)。

基于参数数量的重载

int add(int x, int y)
{
   
    return x + y;
}
int add(int x, int y, int z)
{
   
    return x + y + z;
}
//有几个参数转到那个函数那里去
//基于参数类型的重载
//只要每个重载函数的参数类型集是不同的,也可以区分。
int add(int x, int y); // integer version
double add(double x, double y); // floating point version
double add(int x, double y); // mixed version
double add(double x, int y); // mixed version

因为类型别名(或 typedef)不是不同的类型,所以使用类型别名的重载函数与使用别名类型的重载没有区别。例如,以下所有重载都没有区别(并且会导致编译错误):

typedef int height_t; // typedef
using age_t = int; // type alias
void print(int value);
void print(age_t value); // not differentiated from print(int)
void print(height_t value); // not differentiated from print(int)

对于按值传递的参数,也不考虑 const 限定符。因此,以下功能不被认为是有区别的:

void print(int);
void print(const int); // not differentiated from print(int)

//其中”.......“参数被认为是一种独特的参数类型

void foo(int x, int y);
void foo(int x, ...); // differentiated from foo(int, int)

注意:函数的返回类型不考虑微分,也就是说”double add()“和”int add()“编译器会出现报错行为,根据返回类型不足以编译器识别并确定是哪一个重载函数,意味着还需要更多的分析。

8.11 重载解析

编译器将函数调用与特定的重载函数匹配的过程称为重载解析。

分析一下例子,编译器是不是真的没法匹配到重载函数

#include 
void print(int x)
{
   
     std::cout << x;
}
void print(double d)
{
   
     std::cout << d;
}
int main()
{
   
     print('a'); // char does not match int or double
     print(5l); // long does not match int or double
     return 0;
}

答案是否定,没有完全匹配并不代表着找不到匹配,char或者是long可以隐式转换为int或者是double,但在哪一种情况下,哪一种是最好的转换?

一:解决重载的函数调用

当对重载函数进行函数调用时,编译器会逐步执行一系列规则,以确定哪个(如果有)重载函数最匹配。
在每一步,编译器都会对函数调用中的参数应用一系列不同的类型转换。对于应用的每个转换,编译器检查现在是否有任何重载函数匹配。在应用了所有不同的类型转换并检查匹配之后,该步骤就完成了。结果将是三种可能的结果之一:

  1. 没有找到匹配的函数。编译器移至序列中的下一步。
  2. 找到了一个匹配函数。该功能被认为是最佳匹配。匹配过程到此结束,后续步骤不再执行。
  3. 找到了多个匹配函数。编译器将发出不明确的匹配编译错误。我们稍后会进一步讨论这个案例。
    如果编译器在没有找到匹配的情况下到达整个序列的末尾,它将生成一个编译错误,即找不到匹配的函数调用的重载函数。

二:参数匹配序列

1.编译器尝试找到完全匹配。
首先,查看是否存在重载函数,查看其调用的参数类型与重载函数中的参数类型完全匹配。
举例:

void print(int)
{
   
}
void print(double)
{
   
}
int main()
{
   
    print(0); // exact match with print(int)
    print(3.4); // exact match with print(double)
    return 0;
}
//其中0是和int对应,3.4是和double对应的,所以能够完全匹配

其次,编译器对函数调用中的参数应用一些简单的转换,例如非常量类型可以简单的转换为常量类型。

void print(const int)
{
   
}
void print(double)
{
   
}
int main()
{
   
    int x {
    0 };//int x = 0;
    print(x); // x trivially converted to const int
    return 0;
}
//在此次示例中,调用print(x),x是int类型,编译器将简单的x从int转换为了const int ,然后匹配到了print(const int)。
通过简单转换完成的匹配将被视为完全匹配。

2.如果没有找到完全匹配,编译器将尝试通过参数应用数字提升来找匹配。

void print(int)
{
   
}
void print(double)
{
   
}
int main()
{
   
    print('a'); // promoted to match print(int)
    print(true); // promoted to match print(int)
    print(4.5f); // promoted to match print(double)
    return 0;
}
//对于print('a'),因为print(char)在前面的步骤中找不到与的完全匹配,编译器将字符'a'提升为int,并寻找匹配。这匹配print(int),因此函数调用解析为print(int)。
```3.如果通过数字提升找不到匹配项,编译器会尝试通过数字转换应该于参数来找到匹配项。
```c
#include  // for std::string
void print(double)
{
   
}
void print(std::string)
{
   
}
int main()
{
   
    print('a'); // 'a' converted to match print(double)
    return 0;
}
//在这种情况下,因为没有print(char)(完全匹配),也没有print(int)(促销匹配),将'a'数字转换为双精度并与print(double).

4.如果通过数字转换未找到匹配项,编译器将尝试通过任何用户定义的转换找到匹配项。尽管我们还没有介绍用户定义的转换,但某些类型(例如类)可以定义到其他可以隐式调用的类型的转换。这是一个例子,只是为了说明这一点:

// We haven't covered classes yet, so don't worry if this doesn't make sense
class X
{
   
public:
    operator int() {
    return 0; } // Here's a user-defined conversion from X to int
};
void print(int)
{
   
}
void print(double)
{
   
}
int main()
{
   
    X x;
    print(x); // x is converted to type int using the user-defined conversion from X to int
    return 0;
}
//在这个例子中,编译器会先检查精确匹配是否print(X)存在。我们还没有定义的。接下来,编译器会检查是否x可以促进数字,它不能。然后,编译器将检查是否x可以数字转换,它也不能。最后,编译器会再寻找任何用户定义的转换。因为我们从定义一个用户定义的转换X到int,编译器将其转换X成int相匹配print(int)。
//应用用户定义的转换之后,编译器可以应用隐式转换为找到一个匹配。所以,如果我们的用户定义的转换已经输入char相反,编译器会使用用户定义的转换char,然后推动的结果为int匹配。

5.如果通过用户定义的转换没有发现匹配,则编译器将寻找一个匹配的函数,它使用省略号。

6.如果此时没有找到匹配项,编译器将放弃并发出关于无法找到匹配函数的编译错误。

三:暧昧匹配(不明确匹配)

编译器不能达到完全匹配的效果时,会尝试进行转换进而匹配,但是转换之后发现两个重载函数都可以,那么这个时候函数的调用将被视为是不明确的。

void print(int x)
{
   
}
void print(double d)
{
   
}
int main()
{
   
    print(5l); // 5l is type long
    return 0;
}

由于文字5l是类型long,编译器会先看看,看看它是否可以找到完全匹配print(long),但它不会找到一个。接下来,编译器将尝试数值提升,但类型的值long不能被提拔,所以这里没有比赛无论是。
随后,编译器将尝试通过应用数字转换到找到匹配long的说法。在检查所有的数字转换规则的过程中,编译器会发现两个潜在的匹配。如果long参数进行数值转换成一个int,则该函数调用将匹配print(int)。如果long参数,而不是转换成一个double,那么它将匹配print(double)代替。由于通过数字转换两种可能的比赛已经发现,函数调用被认为是不明确的。

另一个例子

void print(unsigned int x)
{
   
}
void print(float y)
{
   
}
int main()
{
   
    print(0); // int can be numerically converted to unsigned int or to float
    print(3.14159); // double can be numerically converted to unsigned int or to float
    return 0;
}

尽管您可能希望0解析为print(unsigned int)并3.14159解析为print(float),但这两个调用都会导致不明确的匹配。该int值0可以在数字上转换为 anunsigned int或 a float,因此任何一个重载都同样匹配,结果是一个不明确的函数调用。
这同样适用于一个转换double到任何一个float或unsigned int。无论是数字转换,因此无论是过载相匹配同样出色,其结果是再次明确。

8.13 函数模板

提问:假如我们需要使用一个比较大小的函数,该如何实现呢?

int max(int x, int y)
{
   
    return (x > y) ? x : y;
}

double max(double x, double y)
{
   
    return (x > y) ? x: y;
}
//我们意识到对于不同的类型需要重新构造函数,尽管他们的结构一模一样。

所以欢迎来到C++模板!
目的: 简化创建能够与不同的数据类型的功函数(或类)的过程。

我们不是手动创建一堆几乎相同的函数或类(每组不同类型一个),而是创建一个template就像普通定义一样,模板描述了函数或类的样子。与普通定义(必须指定所有类型)不同,在模板中我们可以使用一种或多种占位符类型。占位符类型表示在编写模板时未知的某种类型,但稍后会提供。
一旦定义了模板,编译器就可以根据需要使用模板生成尽可能多的重载函数(或类),每个函数使用不同的实际类型!
最终结果是一样的——我们最终得到了一堆几乎相同的函数或类(每组不同类型一个)。但是我们只需要创建和维护一个模板,编译器就会为我们完成所有繁重的工作。

注意:

  1. 编译器可以使用单个模板来生成一系列相关的函数或类,每个函数或类使用不同的类型集!
  2. 模板可以使用在编写模板时甚至不存在的类型。这有助于使模板代码既灵活又面向未来!

一:函数模板

函数模板是一个函数类定义是,用于生成一个或多个重载函数,每个具有一组不同的实际类型。
当我们创建函数模板时,我们将占位符类型(也称为模板类型)用于任何参数类型、返回类型或稍后要指定的函数体中使用的类型。

要创建函数模板,我们要做两件事。首先,我们将用模板类型替换我们的特定类型。在这种情况下,因为我们只有一种类型需要替换 ( int),所以我们只需要一种模板类型。使用单个大写字母(以 T 开头)来表示模板类型是常见的约定。

int max(int x, int y)
{
   
    return (x > y) ? x : y;
}
//模板
T max(T x, T y) // won't compile because we haven't defined T
{
   
    return (x > y) ? x : y;
}
//这是一个好的开始——但是,它不会编译,因为编译器不知道是什么T!而且这还是一个普通的函数,不是函数模板。

其次,我们要告诉编译器这是一个函数模板,这T是一个模板类型。这是使用所谓的模板参数声明完成的:

template <typename T> // this is the template parameter declaration
T max(T x, T y) // this is the function template definition for max
{
   
    return (x > y) ? x : y;
}

因为这个函数模板有一个名为 的模板类型T,我们将其称为max。
让我们稍微仔细看看模板参数声明。我们从关键字开始template,它告诉编译器我们正在创建一个模板。接下来,我们在尖括号 () 内指定我们的模板将使用的所有模板类型。对于每个模板类型,我们使用关键字typenameor class,后跟模板类型的名称(例如T)。
每个模板函数(或模板类)都需要自己的模板参数声明。

8.14 函数模板实例化

本节内容主要是学习如何使用函数模板
函数模板实际上并不是函数——它们的代码不是直接编译或执行的。相反,函数模板只有一项工作:生成函数(编译和执行)。
关于在上节中使用的max函数,使用语法为:

max<actual_type>(arg1, arg2); 
// actual_type 是某种实际类型,如 int 或 double

举例:

#include 
template <typename T>
T max(T x, T y)//函数模板
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max<int>(1, 2) << '\n'; // instantiates and calls function max(int, int)调用函数模板
    return 0;
}
//当编译器遇到函数调用时max(1, 2),它会确定max(int, int)不存在的函数定义。
//因此,编译器将使用我们的max函数模板来创建一个。

从函数模板(具有模板类型)创建函数(具有特定类型)的过程称为函数模板实例化(或简称实例化)。当这个过程由于函数调用而发生时,它被称为隐式实例化。实例化的函数通常称为函数实例(简称实例)或模板函数。函数实例在各方面都是普通函数。

一:关于函数推导:

在参数类型与我们想要的实际类型匹配的情况下,我们不需要指定实际类型——相反,我们可以使用模板参数推导让编译器从参数类型中推导出应该使用的实际类型在函数调用中。

std::cout << max<int>(1, 2) << '\n'; 
// specifying we want to call max

//函数推导可以修改为
std::cout << max<>(1, 2) << '\n';
std::cout << max(1, 2) << '\n';

在任何一种情况下,编译器都会看到我们没有提供实际类型,因此它将尝试从函数参数中推导出实际类型,这将允许它生成一个max()函数,其中所有模板参数都与所提供参数的类型相匹配. 在此示例中,编译器将推断使用max具有实际类型的函数模板int允许它实例化max(int, int)两个模板参数 (int) 的类型与提供的参数 (int)的类型匹配的函数。

这两种情况之间的区别与编译器如何解析一组重载函数的函数调用有关。在最上面的情况下(带有空的尖括号),编译器max在确定调用哪个重载函数时只会考虑模板函数重载。在底部情况下(没有尖括号),编译器将同时考虑max模板函数重载和max非模板函数重载。

#include 
template <typename T>
T max(T x, T y)
{
   
    return (x > y) ? x : y;
}
int max(int x, int y)
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max<int>(1, 2) << '\n'; // selects max
    std::cout << max<>(1, 2) << '\n'; // deduces max(int, int) (non-template functions not considered)
    std::cout << max(1, 2) << '\n'; // calls function max(int, int)
    return 0;
}

具有非模板参数的函数模板,可以创建具有模板类型和非模板类型参数的函数模板。模板参数可以匹配任何类型,非模板参数就像普通函数的参数一样工作。

template <typename T>
int someFcn (T x, double y)
{
   
    return 5;
}
int main()
{
   
    someFcn(1, 3.4); // matches someFcn(int, double)
    someFcn(1, 3.4f); // matches someFcn(int, double) -- the float is promoted to a double
    someFcn(1.2, 3.4); // matches someFcn(double, double)
    someFcn(1.2f, 3.4); // matches someFcn(float, double)
    someFcn(1.2f, 3.4f); // matches someFcn(float, double) -- the float is promoted to a double
    return 0;
}

这个函数模板有一个模板化的第一个参数,但第二个参数固定为 type double。请注意,返回类型也可以是任何类型。在这种情况下,我们的函数将始终返回一个int值。

实例化的函数可能并不总是编译的

#include 
template <typename T>
T addOne(T x)
{
   
    return x + 1;
}
int main()
{
   
    std::cout << addOne(1) << '\n';
    std::cout << addOne(2.3) << '\n';
    return 0;
}
//2  3.3

但是如果我们尝试做以下的事情呢?

#include 
#include 
template <typename T>
T addOne(T x)
{
   
    return x + 1;
}
int main()
{
   
    std::string hello {
    "Hello, world!" };
    std::cout << addOne(hello) << '\n';
    return 0;
}

当编译器尝试解析时,addOne(hello)它不会找到与之对应的模板函数匹配addOne(std::string),但它会找到我们的函数模板addOne(T),并确定它可以从中生成一个addOne(std::string)函数。因此,编译器将生成并编译:

#include 
#include 
template <typename T>
T addOne(T x);
template<>
std::string addOne<std::string>(std::string x)
{
   
    return x + 1;
}
int main()
{
   
    std::string hello{
    "Hello, world!" };
    std::cout << addOne(hello) << '\n';
    return 0;
}

然而,这将产生一个编译错误,因为x + 1当没有意义x是std::string。这里显而易见的解决方案是不addOne()使用 type 参数进行调用std::string。

二:通用编程

因为模板类型可以替换为任何实际类型,所以模板类型有时被称为泛型类型。并且因为模板可以不可知地编写为特定类型,所以使用模板编程有时称为泛型编程。C++通常非常关注类型和类型检查,相比,泛型编程让我们专注于算法的逻辑和数据结构的设计,而不必太担心类型信息。

三:本节学习结论
  1. 一旦习惯了编写函数模板,您就会发现它们实际上并不会比编写具有实际类型的函数花费更长的时间。函数模板可以通过最小化需要编写和维护的代码量来显着减少代码维护和错误。
  2. 函数模板确实有一些缺点,我们将不提它们。首先,编译器将为每个具有一组唯一参数类型的函数调用创建(并编译)一个函数。因此,虽然函数模板编写起来很紧凑,但它们可能会扩展成大量的代码,这会导致代码膨胀和编译时间变慢。函数模板更大的缺点是它们往往会产生看起来很疯狂、边缘不可读的错误消息,这些错误消息比常规函数更难破译。这些错误消息可能非常令人生畏,但是一旦您理解了它们试图告诉您的内容,它们所指出的问题通常很容易解决。
  3. 与模板为您的编程工具包带来的强大功能和安全性相比,这些缺点是相当小的,所以在需要类型灵活性的任何地方都可以自由地使用模板!一个好的经验法则是首先创建普通函数,如果您发现需要为不同的参数类型重载,然后将它们转换为函数模板。

8.15 具有多种模板类型的函数模板

在上面几节的学习中我们学习了函数模板的相关知识
分析一下例子,程序是否能够运行

#include 
template <typename T>
T max(T x, T y)
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max(2, 3.5) << '\n';  // compile error
    return 0;
}

编译之后你还会发现报错,出现"int",“double”,"不明确"等字样,实际上,我们使用了两种参数类型,一种是int和double。因为我们在不使用尖括号指定实际类型的情况下进行函数调用,所以编译器将首先查看是否存在与max(int, double),它不会找到一个。
接下来,编译器将查看它是否可以找到一个函数模板匹配(使用模板参数推导,我们在第8.14- 函数模板实例化中介绍过)。但是,这也会失败,原因很简单:T只能表示单一类型。没有任何类型T允许编译器将函数模板实例max(T, T)化为具有两种不同参数类型的函数。换句话说,因为函数模板中的两个参数都是 type T,所以它们必须解析为相同的实际类型。
但是此时存在一个疑问,为什么编译器不能生成函数max(double,double),然后将类型int转换成double呢?答案很简单:类型转换仅在解析函数重载的时候进行,而不是在执行模板参数推导的时候进行。这类模式使得编程没有相对复杂化,确保参数类型一致。

下面我们将学习三种方法解决此类问题:

一:三种函数模板

  1. 使用static_cast将参数转换为匹配类型
#include 
template <typename T>
T max(T x, T y)
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max(static_cast<double>(2), 3.5) << '\n'; // convert our int to a double so we can call max(double, double)
    return 0;
}
//现在两个参数都是 type double,编译器将能够实例化
//max(double, double)以满足此函数调用。
  1. 提供实际类型
include <iostream>
double max(double x, double y)
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max(2, 3.5) << '\n'; // the int argument will be converted to a double
    return 0;
}

如果我们编写了一个非模板max(double, double)函数,那么我们将能够调用max(int, double)并让隐式类型转换规则将我们的int参数转换为 adouble以便可以解析函数调用:

但是,当编译器进行模板参数推导时,它不会进行任何类型转换。幸运的是,如果我们指定要使用的实际类型,则不必使用模板参数推导:

#include 
template <typename T>
T max(T x, T y)
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max<double>(2, 3.5) << '\n'; // we've provided actual type double, so the compiler won't use template argument deduction
    return 0;
}

在上面的例子中,我们调用max(2, 3.5). 因为我们已明确指定T应替换为double,所以编译器不会使用模板参数推导。相反,它只会实例化 function max(double, double),然后键入 convert 任何不匹配的参数。我们的int参数将被隐式转换为double。虽然这比static_cast更具可读性,但如果我们在进行函数调用时根本不必考虑类型,那就更好。

  1. 具有多个模板类型参数的函数模板
    我们问题的根源在于,我们只T为函数模板定义了单个模板类型 ( ),然后指定两个参数必须是同一类型。解决这个问题的最好方法是重写我们的函数模板,使我们的参数可以解析为不同的类型。T我们现在将使用两个 (T和U) ,而不是使用一个模板类型参数:
#include 
template <typename T, typename U> // We're using two template type parameters named T and U
T max(T x, U y) // x can resolve to type T, and y can resolve to type U
{
   
    return (x > y) ? x : y; // uh oh, we have a narrowing conversion problem here
}
int main()
{
   
    std::cout << max(2, 3.5) << '\n';
    return 0;
}

实质就是定义了多个模板,T和U可以分别替换成多个类型。但是,上面的代码仍然有一个问题:使用通常的算术规则(8.4 - 算术转换),double优先于int,因此我们的条件运算符将返回double。但是我们的函数被定义为返回T,在T解析为int的情况下,我们的double返回值将进行缩小转换为int,这将产生警告(并可能丢失数据)。
我们如何解决这个问题?这是auto返回类型的一个很好的用途——我们将让编译器从 return 语句推断返回类型应该是什么:

#include 
template <typename T, typename U>
auto max(T x, U y)
{
   
    return (x > y) ? x : y;
}
int main()
{
   
    std::cout << max(2, 3.5) << '\n';
    return 0;
}

二:缩写功能模板

C++20 引入了auto关键字的新用法:当auto关键字在普通函数中用作参数类型时,编译器会自动将函数转换为函数模板,每个自动参数成为独立的模板类型参数。这种创建函数模板的方法称为缩写函数模板。

auto max(auto x, auto y)
{
   
    return (x > y) ? x : y;
}

//C++中的简写
template <typename T, typename U>
auto max(T x, U y)
{
   
    return (x > y) ? x : y;
}//增加可读性

8.16 函数总结

一:总结

  1. 将值从一种数据类型转换为另一种数据类型的过程称为类型转换。
  2. 隐式类型转换(也称为自动类型转换或强制转换)在需要一种数据类型时执行,但提供了不同的数据类型。如果编译器能够弄清楚如何在两种类型之间进行转换,它就会。如果它不知道如何,那么它将失败并出现编译错误。
  3. C++ 语言在其基本类型(以及一些更高级类型的转换)之间定义了许多称为标准转换的内置转换。这些包括数字提升、数字转换和算术转换。
  4. 甲数值提升是较小的数字类型的较大的数值类型(通常是转化int或double),使得CPU可以在匹配了处理器的自然数据大小的数据进行操作。数值提升包括整数提升和浮点提升。数字促销是保值的,这意味着没有价值或精度的损失。
  5. 一个数值转换为基本类型之间的类型转换,是不是一个数值提升。甲缩小转换为数字的转换可能导致的值或精确度的损失。
  6. 在 C++ 中,某些二元运算符要求其操作数的类型相同。如果提供了不同类型的操作数,将使用一组称为常用算术转换的规则将一个或两个操作数隐式转换为匹配类型。
  7. 当程序员通过强制转换显式请求转换时,将执行显式类型转换。甲铸表示由程序员的请求做一个显式的类型转换。C ++支持5种类型的铸件:C-style casts,static casts,const casts,dynamic casts,和reinterpret casts。通常你应该避免C-style casts, const casts, 和reinterpret casts。 static_cast用于将值从一种类型转换为另一种类型的值,并且是迄今为止 C++ 中最常用的转换。
  8. Typedefs和Type aliases允许程序员为数据类型创建别名。这些别名不是新类型,其作用与别名类型相同。Typedef 和类型别名不提供任何类型的安全性,需要注意不要假设别名与其别名的类型不同。
  9. 该自动关键字有多种用途。首先, auto 可用于进行类型推导(也称为类型推断),这将从变量的初始值设定项中推导出变量的类型。类型推导删除 const 和引用,因此如果需要,请确保将它们添加回来。
  10. Auto 也可以用作函数返回类型,让编译器从函数的 return 语句推断函数的返回类型,尽管对于普通函数应该避免这种情况。Auto 用作尾随返回语法的一部分。
  11. 函数重载允许我们创建多个具有相同名称的函数,只要每个同名函数具有不同的参数类型集(或者函数可以以其他方式区分)。这样的函数称为重载函数(或简称重载)。不考虑返回类型进行区分。
  12. 在解析重载函数时,如果没有找到精确匹配,编译器将优先选择可以通过数字提升匹配的重载函数,而不是需要数字转换的函数。当对已重载的函数进行函数调用时,编译器将尝试根据函数调用中使用的参数将函数调用与适当的重载相匹配。这称为重载决议。
  13. 一个不明确的匹配时,编译器发现可在函数调用相匹配重载函数,不能确定哪一个是最好的两个或更多功能发生。
  14. 甲默认参数是提供了一种用于一个函数参数的默认值。带有默认参数的参数必须始终是最右边的参数,并且在解析重载函数时不用于区分函数。
  15. 函数模板允许我们创建一个类似函数的定义,作为创建相关函数的模式。在函数模板中,我们使用模板类型作为稍后要指定的任何类型的占位符。告诉编译器我们正在定义模板并声明模板类型的语法称为模板参数声明。
  16. 从函数模板(具有模板类型)创建函数(具有特定类型)的过程称为函数模板实例化(或简称实例化)。当这个过程由于函数调用而发生时,它被称为隐式实例化。实例化的函数称为函数实例(或简称实例,有时称为模板函数)。
  17. 模板参数推导允许编译器从函数调用的参数中推导出应该用于实例化函数的实际类型。模板参数推导不进行类型转换。
  18. 模板类型有时称为泛型类型,使用模板编程有时称为泛型编程。
  19. 在 C++20 中,当 auto 关键字在普通函数中用作参数类型时,编译器会自动将函数转换为函数模板,每个 auto 参数成为独立的模板类型参数。这种创建函数模板的方法称为缩写函数模板。

									   2021年8月16日

九 复合类型

9.1 枚举

举例

// Define a new enumeration named Color
enum Color
{
   
    color_black,
    color_red,
    color_blue,
    color_green,
    color_white,
    color_cyan,
    color_yellow,
    color_magenta, 
}; 
Color paint = color_white;
Color house(color_blue);
Color apple {
    color_red };

定义枚举(或任何用户定义的数据类型)不会分配任何内存。当定义了枚举类型的变量(如上例中的变量paint)时,此时会为该变量分配内存。请注意,每个枚举器以逗号分隔,整个枚举以分号结束。

一:命名枚举和枚举器

为枚举提供的名称是可以自行选择的,没有名字的枚举有时被称为匿名枚举,通常枚举我们都用大写开头。
我们必须为枚举器指定名称,并且一般使用和常量变量相同的名称样式,有时枚举器以ALL_CAPS命名,但一般不这样做,因为他可能会与预处理器宏有名称冲突的危险。

二:枚举器的作用域

由于枚举器与枚举放置在同一命名空间中,因此不能在同一命名空间内的多个枚举中使用枚举器名称:

enum Color
{
   
  red,
  blue, // blue is put into the global namespace
  green
};
enum Feeling
{
   
  happy,
  tired,
  blue // error, blue was already used in enum Color in the global namespace
};

因此,使用标准前缀作为枚举器的前缀是很常见的,防止命名冲突也可以用于代码目的。

三:枚举值

每个枚举器会根据其在枚举列表中的位置自动分配一个整数值。默认情况下,第一个枚举器被分配整数值 0,并且每个后续枚举器的值都比前一个枚举器大 1:

enum Color
{
   
    color_black, // assigned 0
    color_red, // assigned 1
    color_blue, // assigned 2
    color_green, // assigned 3
    color_white, // assigned 4
    color_cyan, // assigned 5
    color_yellow, // assigned 6
    color_magenta // assigned 7
};
Color paint{
    color_white };
std::cout << paint;
//cout输出打印4

可以显式定义枚举器的值。这些整数值可以是正数或负数,并且可以与其他枚举数共享相同的值。任何未定义的枚举器都被赋予一个比前一个枚举器大 1 的值。
也就是说,可以自由定义枚举器的值,任何数值都可以,并且一个值可以被多个枚举器共享使用,任何未被定义的枚举器都被赋予一个比前一个枚举器大1的值。

// define a new enum named Animal
enum Animal
{
   
    animal_cat = -3,
    animal_dog, // assigned -2
    animal_pig, // assigned -1
    animal_horse = 5,
    animal_giraffe = 5, // shares same value as animal_horse
    animal_chicken // assigned 6
};

请注意,在这种情况下,animal_horse 和animal_giraffe 被赋予了相同的值。当这种情况发生时,枚举变得不明确——本质上,animal_horse 和animal_giraffe 是可以互换的。尽管 C++ 允许这样做,但通常应避免为同一枚举中的两个枚举器分配相同的值。
在原则上我们不为枚举器分配特定的值,除非你一定有理由为他赋值。

四:枚举类型评估和输入/输出

int mypet{
    animal_pig };
std::cout << animal_horse; 
// evaluates to integer before being passed to std::cout
//输出结果为5

//编译器不会将整数隐式转换为枚举值。以下将产生编译器错误:
Animal animal{
    5 }; // will cause compiler error

编译器也不会让您使用 std::cin 输入枚举:

enum Color
{
   
    color_black, // assigned 0
    color_red, // assigned 1
    color_blue, // assigned 2
    color_green, // assigned 3
    color_white, // assigned 4
    color_cyan, // assigned 5
    color_yellow, // assigned 6
    color_magenta // assigned 7
};
Color color{
   };
std::cin >> color; // will cause compiler error

一种解决方法是读入一个整数,并使用 static_cast 强制编译器将整数值放入枚举类型:

int inputColor{
   };
std::cin >> inputColor;
Color color{
    static_cast<Color>(inputColor) };

每个枚举类型都被视为不同的类型。因此,尝试将枚举类型从一种枚举类型分配给另一种枚举类型将导致编译错误:

Animal animal{
    color_blue }; 
// will cause compiler error

如果您想为枚举器使用不同的整数类型,例如在网络枚举器时节省带宽,您可以在 enum 声明中指定它。

// Use an 8 bit unsigned integer as the enum base.
enum Color : std::uint_least8_t
{
   
    color_black,
    color_red,
    // ...
};

由于枚举数通常不用于算术或比较,因此使用无符号整数是安全的。当我们想要转发声明一个枚举时,我们还需要指定枚举基数。

enum Color; // Error
enum Color : int; // Okay
// ...
// Because Color was forward declared with a fixed base, we
// need to specify the base again at the definition.
enum Color : int
{
   
    color_black,
    color_red,
    // ...
};

与常量变量一样,枚举类型会出现在调试器中,这使得它们在这方面比 #defined 值更有用。

五:打印枚举器

正如我们在上面看到的,尝试使用 std::cout 打印枚举值会导致打印枚举器的整数值。那么如何将枚举器本身打印为文本呢?一种方法是编写一个函数并使用 if 或 switch 语句:

enum Color
{
   
    color_black, // assigned 0
    color_red, // assigned 1
    color_blue

你可能感兴趣的:(C++,c++)