系列文章目录
C++ primer plus 第一节 步入C++-CSDN博客
C++ primer plus 第二节 hello world刨析-CSDN博客
C++ primer plus 第三节 数据处理-CSDN博客
C++ primer plus 第四节 复合类型-CSDN博客
C++ primer plus 第五节 循环-CSDN博客
C++ primier plus 第七节 函数探幽第一部分-CSDN博客
目录
系列文章目录
前言
一 引用的绪论
二 引用 类 继承
三 默认参数
四 右值 左值 左引用 右引用
五 函数的重载
六 函数模板
七 重载的模板
八 函数模板的局限性质
九 实例化和具体化
总结
前言
这里是上一篇文章的绪论,其实还要写一篇,太多知识点了,这个本书很难,只能说作者看的要废了,然后这个是基于作者的理解,然后部分后面讲的省掉了,只找重点,让我们理解这里面的重点知识
一 引用的绪论
1 将引用运用到结构体
其实引用就是运用在结构体和类里面的
使用结构体引用参数的方式与变量引用基本相同,只需要在使用的时候加一个运算符&就好了,
例如struct free throws { std: :string name; int made; ìnt attempts; float percent; };
我们就以这个为例子写一个关于结构体引用的知识点的程序
display(accumulate(team, two));
我们可以看到有三个功能函数
这个结构体的含义是每一个人都有成功次数和尝试次数,然后计算出百分比
set_pc是用来进行进行计算百分比的
这个不能用按值传递的,我们直接用引用吗,这样就可以直接对one进行操作了,另外一个操作就是指针写法了,我们可以感受到这样写很简单便捷
display是用来进行显示的
这里直接用const修饰就好,因为我要确定好那些要加const,那些不需要加const,加了const是不需要进行修改的,没有加是一般是要对其有修改操作的
computer是计算一个小组里面的整体的百分比和各个数值的
我们需要操作的six是直接进行引用不加const的
但是我们不需要进行操作的one是直接加上const的,这样安全性更高
当然还有别的写法,由于虚拟机太麻烦了,好难敲,所以我就直接在这里说了display(accumulate(team, two));
这个就是我的computer函数,但是我的修改一下,返回值为结构体引用,加一个return就好了,然后就是直接利用返回值就好了
accumulate (accumulate (team, three) , four);
这个也就是利用返回值的类型和第一个形参的返回值类型相同,这样我们就可以提高自己写代码的效率,等效于下面这个代码
accumulate (team, three) ; accumulate (team, four) ;
首先我们总结一下我们这里所学到的知识点
👉1 我们可以利用返回值类型和形参类型相同,进行代码的简化
👉2 为什么要返回引用类型?
首先我们直到我们在返回操作的时候,是需要额外开辟一个变量进行存储,然后在赋值个那个对应的变量的,这样就可以减少开销
👉3 返回时候需要注意的问题const free throws & clone2(free throws & ft){ free_throws newguy; // first step to big error newguy = ft; // copy info return newguy; // return reference to copy }
我们看我们这里也是返回了结构体引用类型,但是这个是返回其他函数里面其他函数的临时结构体变量,但是这个变量函数销毁的时候会没有的,那么就是返回一个空,就是什么也没有了,那么就是返回不了
👉4 const的使用
我们在使用const修饰的时候是需要好好考虑的,需要考虑这个是否需要修改,还要考虑这个安全性
2 将引用用于类对象
将类对象传递给函数时, C++通常的做法是使用引用。例如,可以通过使用引用,让函数将类string、ostream、 lstream、 ofstream 和 ifstream 等类的对象作为参数
下面来看一个例子,它使用了 string 类,并演示了一些不同的设计方案,其中的一些是糟糕的
这个是用来进行对于字符串的升级
首先我们我们这里由三个升级方式,接下来我们会一个一个讲解
首先第一个升级方式version1
这里是创建了一个临时的变量temp,然后利用字符串的运算,然后得到最后升级之后的字符串
第二个升级方式version2
这里形参是利用了引用,一个是带有const,一个是不带有const修饰的
然后就是直接对上面的变量进行操作,所以input被改变了,注意返回类型,这个s1是存在的,所以可以返回
第三个升级方式version3
这里也是利用引用,但是是用了一个临时变量,这里返回类型也是引用就是犯了我上面说的错误,类似于指针的错误,野指针,但是不是完全是,只是有点像
这里是类的操作的话,还行跟讲结构体差不多,但是我们往深入想就又会又疑问
❌ 注意事项
首先我们上面的*** ### @@@都是char*类型,但是这里形参类型是string,我们需要怎么理解呢?
首先,C++定义了一个由char*类型可以转换到string类型,这使得C-风格字符串类型来初始化string对象
其次就是const形参得一个属性就是,假设形参和实参得类型不匹配得话,但是可以转换为与引用类型相同的类型,因为可以创建一个临时的变量进行存储,这样就可以利用引用引用那个临时变量
二 引用 类 继承
首先在ostream和ofstream类凸显了一个引用的一个有趣的属性,ofstream对象可以使用ostream类的方法,这使得文件输入和输出的格式与控制台输入输出的形式是相同的,使得能够将特性从一个类传递给另一个类的语言特性被称作继承
ostream是基类,ofstream是派生类,派生类继承了基类的方法,这意味ofstream可以使用ostream类的方法
派生类的对象可以使用基类的方法
这个继承后续会仔细进行讲解,这里只需要了解这么多就好了
总的来说,派生类可以使用基类的方法,并且使用的方法格式都是一样的,如意味着ofstream 对象可以使用基类的特性,如格式化方法precision()和 setf( ),还要理解派生类和基类的具体的概念是干嘛的
这里主要是理解,继承的另一个特征,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实 际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可 以将派生类对象作为参数总的来说就是可以用基类引用作为一个函数的参数,传参的时候可以是派生类还可以是基类
接下来用一个物镜目镜求取焦距的程序来梳理这个知识
这里说明一下,这本书用来很多的格式化输出,这个是我们还没有学习的,而且后面还会讲解,所以这里是删改版本,我们这里主要是学习基类引用与派生类和基类的关系
前面加一个函数声明,作者忘记加了
然后我们可以看到这个
代码的上面是创建fstream变量和判断是否可以打开这个文件,这个代码才是关键,我们自己写一个函数,然后是这样的
这个第一个形参是基类的引用,我们上面学习了,基类的引用是可以接受派生类和基类传参的,所以这里我们直接用基类的引用就好了,这里主要是学习这个
何时使用引用参
使用引用参数的主要原因有两个:
程序员能够修改调用函数中的数据对象
通过传递引用而不是整个数据对象,可以提高程序的运行速度|
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用于指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。
那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?
下面是一些指导原则:
对于使用传递的值而不作修改的函数
如果数据对象很小,如内置数据类型或小型结构,则按值传递。
如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向 const的指针。
如果数据对象是较大的结构,则使用 const指针或const引用,以提高程序的效率。这样可以节省 复制结构所需的时间和空间。
如果数据对象是类对象,则使用 const 引用。类设计的语义常常要求使用引用这是 C++新增这 项特性的主要原因
因此,传递类对象参数的标准方式是按引用传递
对于修改调用函数中数据的函数:
如果数据对象是内置数据类型,则使用指针。如果看到诸如 fixit(&x)这样的代码〈其中x是int), 则很明显, 该函数将修改x
如果数据对象是数组,则只能使用指针
如果数据对象是结构,则使用引用或指针
如果数据对象是类对象,则使用引用
三 默认参数
C++新增了一个内容——默认参数
比如我们由一个函数wow(int n),我们在调用这个函数的时候,没有进行给n进行设置值,那么这个n就会使用默认参数,比如这个这个n的默认参数为1,那么我们在调用的时候,用wow()这个形式,那么这个就是为1
下面我们将一个具体的例子
比如由一个这个函数left("theory",3)我们在传参的时候,这个意思就是我要取第几个字母,那么这个运行出来取了theory的e这个字母,假设这个函数的默认参数为1,那么我们调用的时候,使用left("theory")的时候,我们直接取出来t这个字母
这个默认参数给我们提供了一个很大的便捷的方式,如果程序每次都是取出一个字母,而不是一个长的字符串,那么就是极大减少了我们的操作
怎么设置默认参数
首先这个默认参数的设置是在函数的原型进行设置的,即函数的声明那里,比如void left(char* str, int n = 1);
这个例子就是设置默认参数n为1,那么我们下面以一个实际的程序来理解默认参数
![]()
![]()
我们可以看到上面这个代码,首先这个main函数有两个实例的程序,一个是取到第四个字母,就是从第一个取到第四个字母,然后第二个样例就是使用默认参数来输出,这个默认参数在函数原型进行设置了,为1
然后我们来看一下这个left功能函数,这个函数的主要功能为把对应的字母放入到new开辟的空间里面,然后利用循环进行输出
第一个for循环是i<n的时候就直接停止,但是第二个条件是用来干嘛的呢?
如果我们输入了一个本身字符串还大的值,比如10000,如果没有第二个条件,那么程序运行会出错,所以我们这个作用是弄到规定的字符串大小就进行停止
第二个while就是把我们后面没有弄到的设置为\0,这样有利于cout识别输出,因为cout是根据\0识别进行输出的
程序效率的修改
我们来看这个程序就是这个new开辟如果我们输入10000,那么是不是有很多的内存浪费,所以我们想要有一个节省内存的
方法一
这个是根据strlen进行计算元素的具体大小来进行判断,但是这个过程有函数调用的开销和返回值的保存这样就会导致效率降低,但是内存省了很多
我们既要节省内存也要体高效率就直接进行自己的书写
我们直接用这个程序就好了计算m的大小,当满足上述条件就好了
小结
我们直到默认参数的设置与好处,还用了一个程序来举例
其次就是我们学习了前面的知识,我们不仅要写出代码,还要提高函数的效率,这样才可以加深我们的知识的运用
四 右值 左值 左引用 右引用
由于函数的重载在这个没有很详细的介绍,导致后面很多都看不懂,所以为了理解函数的重载,我们这里先理解这个,这个右引用这里先知道,后面会进行很详细的介绍
左值和右值int a = 10; int b = a; int c = (a+b); a+c = c; 10 = a;
我们先来看几个代码
a=10,这个10就是右值,这个a就是左值
b=a,这个b就是左值,a就是右值,这个是比较特殊的例子
c=a+b,这个就是c就是左值,a+b就是右值
但是不可以像我们最后两个所写的这样
所以我们可以得出一个小的结论
左值一般都是可以取地址的,右值一般都是不可以取地址的
左引用和右引用int b = 10; int a = 10; int& e = b; int& c = 10; int& d = (a+c); const int& o = 10; const int& p = (a+c);
首先第三行的是我们很常规的写法,这个就是对于一个变量的引用嘛,这个右边的不就是我们上面所说的左值嘛,那么这个就是左值引用
但是我们看到第四个和第五个的时候就是右边是右值,这个就是不正确的,因为我们这个是左值引用
根据前面所学的就是前面加一个const就可以了,这个是用了一个临时变量,然后引用那个临时变量,但是这个有个缺点就是无法修改,这可以读取,那么就是推出了右值引用int a = 10; int b = 10; int &&c = 10; int &&d = (a+b);
这个就是右值引用,这个我们就可以看懂我们书本后面的代码了,这个就可以修改那个右值嘛
五 函数的重载
C++在C的基础上新加上了一个功能,这个功能的名字也是十分的便于理解,就是一个函数的名称可以在多次的使用,默认参数让您能够使用不同数目的参数调用同一个 函数,而函数多态(函数重载〉让您能够使用多个同名的函数,这个函数的多态也就是函数的重载,只是函数的重载更加标准
函数重载的标准特征——函数的参数列表
参数的个数是使用边缘器进行计算得出来的
函数参数列表的不同包括:1 参数的个数 2 参数的顺序 3 参数的类型
然后我会分别举例出函数重载的问题情况
1 函数重载的样例
这个是一些函数重载的样例
这个十分的简单,我们下面来看一些比较容易出错的
2 函数未找到对应的参数类型
比如这个我想在上面那五个函数里找打,这个usigned int是未找到的,那么这个时候你以为编译器停止这个工作进行报错了嘛?其实不然
它不与任何原型匹配! 没有匹配的原型并不会自动停止使用其中的某 个函数,因为C++将尝试使用标准类型转换强制进行匹配。 如果#2原型是print()唯一的原型,则函数调用 print(year, 6)将把 year 转换为 double 类型
但是读者可能去尝试之后还是会报错,不是都说可以进行强制转换了嘛?为什么还是会进行报错?原因就是这里有2,3,4这三个都可以进行转换,那么C++就会不接受这种传参形式,也因此会进行报错,除非只有一个可以进行转换的
3 引用
当我们在使用函数重载里面放入引用这个时候有相同的类型,那么这个时候其实编译器是视为一样的,那么也会提示报错,因为不是唯一的,这里要特别注意,因为后续使用引用的情况特别多,所以就很容易出错
4 const和非const
首先编译器不会进行const与非const的区分,但里面还是会有一些需要注意的东西
首先函数的参数是由const修饰,那么你传参数,char和const char都是寻找const char进行函数调用
但是你的函数参数是char,那么const char就不行,const char会去寻找带有const的,但是char会去寻找不带有const的
5 重载引用参数
函数重载的特别
首先我们看到这个,也就是说左值引用会去找左值,右值引用则会去寻找右值,但是右值找不到右值引用的时候,就会去找带有const的函数
C++函数重载里面有一个很神奇的东西,虽然你写的名称是一样的,但是也会进行名词修饰或名称矫正
名称矫正或者名词修饰就是指把函数的名称进行稍微的修改
六 函数模板
我们直到函数的重载,那么我们知道,当我们在改写很多的不同的数据类型的时候,是要进行复制黏贴,这样的操作虽然可以实现函数名字的不冲突,但是用起来还是十分的麻烦,有时候后面的代码可能产生错误信息,那么我们就又引入了一个新的特性——函数模板
我们知道C++是支持泛型编程的,所以支持泛型编程的话,那么也就存在一个函数可以使用多个类型的传参template <typename AnyType> void Swap (AnyType &a, AnyType &b){ AnyType temp; temp = a; a = b; b = temp;}
下面这个代码是一个函数模板的样例,这个首先要用到这个template,然后后面写一个typename 然后类型的名字,一般程序员会把这个名字简化写出T
然后我们在看别的代码的时候也会遇到或者typename写成class,这个是C++98之前所使用的了,然后这里C++98之后就加上了typename这个东西
首先我们在书写这个类型的时候,我们一般都是建议输入typename的
如果需妥多个将同一种算法用于不同类型的函数,请使用模板。 如果不考虑向后兼容的问题, 并愿意键入较长的单词,则声明类型参数时,应使用关键字可pename而不使用 class
下面这是根据函数模板进行编写的程序#include<iostream> #include<string> using namespace std; template <typename T> void Iswap(T& a, T& b) { T temp = a; a = b; b = temp; } int main() { int a = 10; int b = 20; Iswap(a, b); cout << "This is int swap : " << endl; cout << "a = " << a << " b = " << b << endl; double x = 19.8; double y = 20.9; Iswap(x, y); cout << "This is double swap : " << endl; cout << "x = " << x << " y = " << y << endl; return 0; }
我们可以看到这个函数的模板函数是怎么设置的
其实我们函数模板每次调用一个不同类型的数据的时候,都是会创建一个新的函数来进行接受,这个发生在编译阶段,就比如我们这里就会有一个int类型的交换函数,还有一个double类型的交换函数,这个的存在就是为了避免函数重载写多个有代码的陈杂,然后就是减少出错的几率,更安全,更可靠
七 重载的模板
我们学习过函数的模板和函数的模板,那么重载模板就是对函数的模板进行重载,我们上述是可以进行交换值,如int, char, double, float, long这几个类型,但是对于数组的时候就不行了,但是算法还是相同的,那么我们就对模板进行重载
下面我们来对上面那个函数的模板进行重载#include<iostream> #include<string> using namespace std; template <typename T> void Iswap(T a[], T b[], T n) { for (int i = 0; i < n; i++) { T temp = a[i]; a[i] = b[i]; b[i] = temp; } } template <typename T> void Iswap(T& a, T& b) { T temp = a; a = b; b = temp; } void show(int a[], int n) { for (int i = 0; i < n; i++) cout << a[i] << " "; cout << endl; } int main() { int a = 10; int b = 20; Iswap(a, b); cout << "This is int swap : " << endl; cout << "a = " << a << " b = " << b << endl; double x = 19.8; double y = 20.9; Iswap(x, y); cout << "This is double swap : " << endl; cout << "x = " << x << " y = " << y << endl; int n = 6; int ao[6] = { 1,2,3,4,5,6 }; int bo[6] = { 6,5,4,3,2,1 }; cout << "array a : " << endl; show(ao, n); cout << "array b : " << endl; show(bo, n); cout << "This is a swap array : " << endl; Iswap(ao, bo, n); cout << "array a : " << endl; show(ao, n); cout << "array b : " << endl; show(bo, n); return 0; }
这里是实现数组的互换的,我们可以看到我们实现重载的模板
八 函数模板的局限性质
我们在声明函数模板的时候,前面的声明是需要数学template <typename T>的
下面我们来看一个代码#include<iostream> #include<string> using namespace std; struct Cengineer { string name; int salary; int floor; }; template <typename T> void Iswap(T& a, T& b); void show(Cengineer people); int main() { int a = 10; int b = 20; Iswap(a, b); cout << "This is int swap : " << endl; cout << "a = " << a << " b = " << b << endl; double x = 19.8; double y = 20.9; Iswap(x, y); cout << "This is double swap : " << endl; cout << "x = " << x << " y = " << y << endl; Cengineer Jake = { "Jake",10000,10 }; Cengineer Mike = { "Mike",11000,15 }; cout << "engineer's job and salary : " << endl; show(Jake); show(Mike); cout << "swap data : " << endl; show(Jake); show(Mike); return 0; } void show(Cengineer people) { cout << "name : " << people.name << " salary : " << people.salary << " floor : " << people.floor << endl; } template <typename T> void Iswap(T& a, T& b) { T temp = a; a = b; b = temp; }
首先我们可以看到我们这一次交换的是结构体,但是我想的是利用这个Iswap进行交换,但是结果是名字,薪水,楼层都被互换了,所以导致交换失败,这个时候我们就想到利用函树的重载,但是,我们知道我们声明的时候还是这个形式
template <typename T> void Iswap(T& a, T& b);
这就使这个重载不了,我们上面也学习过,引用是不会作为特征进行处理的,所以这里我们就会陷入困境
那么这个时候就会有显示具体化
显示具体化
什么是显示具体化呢?
C++可以提供一个具体化函数定义一一称为显式具体化 (explicitspecialization), 其中包含所需的代 码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板
我们以一个例子来讲解#include<iostream> #include<string> using namespace std; struct Cengineer { string name; int salary; int floor; }; template <typename T> void Iswap(T& a, T& b); template <> void Iswap<Cengineer>(Cengineer& a, Cengineer& b); void show(Cengineer people); int main() { int a = 10; int b = 20; Iswap(a, b); cout << "This is int swap : " << endl; cout << "a = " << a << " b = " << b << endl; double x = 19.8; double y = 20.9; Iswap(x, y); cout << "This is double swap : " << endl; cout << "x = " << x << " y = " << y << endl; Cengineer Jake = { "Jake",10000,10 }; Cengineer Mike = { "Mike",11000,15 }; cout << "engineer's job and salary : " << endl; show(Jake); show(Mike); cout << "swap data : " << endl; show(Jake); show(Mike); return 0; } void show(Cengineer people) { cout << "name : " << people.name << " salary : " << people.salary << " floor : " << people.floor << endl; } template <typename T> void Iswap(T& a, T& b) { T temp = a; a = b; b = temp; } template <> void Iswap<Cengineer>(Cengineer& a, Cengineer& b) { int c = 0; int d = 0; c = a.salary; a.salary = b.salary; b.salary = c; d = a.floor; a.floor = b.floor; b.floor = a.floor; }
看到这一个
template <> void Iswap<Cengineer>(Cengineer& a, Cengineer& b);
这个是用来进行显示具体化的声明
这个template的尖括号里面是不需要输入任何东西,然后这个后面要加一个尖括号,里面写上具体的类型,这样就可以让编译器知道哪一种是需要额外进行操作的,然后就把这个传进去进行操作就好了
编译器是先执行显示化还是先执行模板呢?
试验其他具体化方法后, C++锦标准选择了下面的方法
• 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本
• 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型
• 具体化优先于常规模板,而非模板函数优先于具体化和常规模板
九 实例化和具体化
定义
编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation )。例如,在程序中,函数调用 Swap(i,j)导致编译器生成 Swap( )的一个实例,该实例使用int类型。模板并非函数定义,但使用int的模板实例是函数定义,这种实例化方式被称为隐式实例化, 因为编译器之所以知道需要进行定义,是由于程序调用 Swap( )函数时提供了 mt参数。 最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化Cexplicit instantiation) 。这意味着可以直接命令编译器创建特定的实例,如 Swap( )。其语法是,声明所需的种 类一一用o符号指示类型,并在声明前加上关键字templatetemplate void Swap<int>(int , int); // explicit instantiation
这个就是模板的实例化
然后我们对比一下具体化有什么不用呢?template <> void Swap<int> (int &, int &); // explicit specialization template <> void Swap(int &, int &); // explicit specialization
综上所述
实例化分为隐式实例化和显示实例化,隐式实例化就是指当你必须传递参数,告知这个参数的类型为什么的时候,这个时候编译器才会把这个模板里面的int类型的函数实例化,但是显示实例化,无论你有没有传递参数,这个编译器都会进行实例化
具体化是用在告诉编译器这个需要进行特殊的处理
显示实例化声明的是,前面的template是不带有尖括号的
总结
引用的绪论
我们可以知道结构体和类的引用的使用方法,什么时候使用引用
引用我们需要注意的就是const修饰,返回值的类型,用const修饰的时候,计算机会进入哪一些操作,提示临时变量
引用,类,继承
我们这里需要知道基类,派生类是什么含义
基类的引用作为形参的话,进行传实参的时候,既可以是派生类或者基类
继承的简介,就是可以让派生类使用基类的特性
什么时候使用值传递,什么时候使用指针传递,什么时候使用引用传递
默认参数
默认参数的使用是在函数原型的时候,给变量赋予初始值,然后当我们不在这个函数的默认参数进行传值的话,那么就是使用默认参数,当我们传参的时候,传入了值,那么这个默认参数是会被掩盖的,这个是运用到重载和安全还有就是便捷
左值,右值,左值引用,右值引用
函数的重载
函数的重载就是可以避免函数的名字冲突,这里里卖弄的函数特征是函数的参数列表
包括个数,数量,类型,接下里就是几个特例
引用和相同类型
const修饰
当未找到参数类型时候,就会判断是否可以进行强制转换,如果没有或者有多个可以,那么就是直接进行报错
函数的模板
由于函数的重载很容易报错,就有了这个模板
template <typename T>
void swap(T &a,T &b);
template <typename T>
void swap(T &a,T &b){
······
}
这个是函数模板
重载的模板
由于函数模板有时候处理不了更多的数据,那么这个时候就会由一个重载,这个就是修改一下函数里面形参的内容,就好了,跟函数的重载一样的
函数模板的局限性
当我们无法进行重载的时候,我们就要考虑使用显示具体化了,这个就是可以帮助我们对于单独一个类型进行书写
template <> void swap <int> (int &a ,int &b){
.......
}
实例化和显示具体化
这个就是一个概念
template void Swap<int>(int , int); // explicit instantiation
实例化分为隐式实例化和显示实例化,隐式实例化就是指当你必须传递参数,告知这个参数的类型为什么的时候,这个时候编译器才会把这个模板里面的int类型的函数实例化,但是显示实例化,无论你有没有传递参数,这个编译器都会进行实例化
具体化是用在告诉编译器这个需要进行特殊的处理
显示实例化声明的是,前面的template是不带有尖括号的