左值,右值,野指针,悬空指针,nullptr,null,强制类型转换,判断结构体是否相等,模板,模板特化

左值和右值的区别,左值引用和右值引用的区别,如何将左值转换成右值

左值: 指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象

右值: 表达式结束就不再存在的临时对象,不可以取地址,没有名字

左值和右值的区别:

左值持久,右值短暂

左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象

由于右值引用只能绑定到临时对象,我们得知

  • 所引用的对象将要被销毁
  • 该对象没有其他用户

这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态

变量是左值

变量表达式都是左值,我们不能将一个右值引用绑定到一个右值引用类型的变量上。

int&& rr1 = 42;   //正确,字面常量是右值
int&& rr2 = rr1;  //错误,表达式rr1是左值!

变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行

左值引用和右值引用的区别:

  • 左值引用不能绑定到要转换的表达式,字面值常量或返回右值的表达式。右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
  • 右值引用必须绑定到右值的引用,通过&&获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。

std::move可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,用于移动语义。

std::move()函数的实现原理

std::move()函数原型:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

说明:引用折叠原理

  • 右值传递给上述函数的形参T&&依然是右值,即T&& &&相当于T&&;
  • 左值传递给上述函数的形参T&&依然是左值,即T&& &相当于T&

总结:通过引用折叠原理可以知道,move()函数的形参既可以是左值也可以是右值。

remove_reference具体实现

//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义 T 的类型别名为 type
};
 
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 

举例:

int var = 10; 

转化过程:
1. std::move(var) => std::move(int&& &) => 折叠后 std::move(int&)

2. 此时:T 的类型为 int&,typename remove_reference<T>::type 为 int,这里使用 remove_reference 的左值引用的特例化版本

3. 通过 static_cast 将 int& 强制转换为 int&&

整个std::move被实例化如下
string&& move(int& t) 
{
    return static_cast<int&&>(t); 
}

总结:

std::move()实现原理:

  • 利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。
  • 然后通过remove_reference移除引用,得到具体的类型T;
  • 最后通过static_cast<>进行强制类型转换,返回T&&右值引用

例子:

string s1("hi!"), s2;
s2 = std::move(string("bye!"));  //正确,从一个右值移动数据
s2 = std::move(s1);   //正确,但在赋值之后,s1的值是不确定的

std::move是如何工作的

​ 在第一个赋值中,传递给move的实参是string的构造函数的右值结果string("bye!")。当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。因此,在std::move(string("bye!"))中:

  • 推断出的T的类型为string
  • 因此,remove_referencestring进行实例化
  • remove_reference<string>type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型为string&&

因此,这个调用实例化move<string>,即函数

string&& move(string&& t)

函数体返回static_cast<string&&>(t)t的类型已经是string&&,于是类型转换什么都不做。因此,此调用的结果就是它所接受的右值引用。

现在考虑第二个赋值,它调用了std::move()。在此调用中,传递给move的实参是一个左值。这样:

  • 推断出的T的类型为string&(string的引用,而非普通string)。
  • 因此,remove_referencestring&进行实例化。
  • remove_reference<string&>type成员是string
  • move的返回类型仍是string&&
  • move的函数参数t实例化为string& &&,会折叠为string&

因此,这个调用实例化move<string&>,即

string&& move(string& t)

这正是我们所寻求的,我们希望将一个右值引用绑定到一个左值。这个实例的函数体返回static_cast<string&&>(t)。在此情况下,t的类型为string&cast将其转换为string&&

什么是野指针和悬空指针?

悬空指针:

  • 若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为悬空指针
void* p = malloc(size);
free(p); //此时,p指向内存空间已释放,p就是悬空指针

野指针:

野指针是指不确定其指向的指针,未初始化的指针为“野指针”。
指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会指向随机区域,因为任何指针变量在被定义的时候是不会被置空的,它的默认值是随机的

void* p;
// p是野指针

野指针的问题在于,指针所指向的内存已经无效了,而指针没有被置空,解引用一个非空的无效指针是一个未被定义的行为,也就是说不一定导致段错误,野指针很难定位到是哪里出现的问题,在哪里这个指针就失效了,不好查找出错的原因,所以调试起来会很麻烦

产生原因及解决方法
野指针:指针变量未及时初始化=》定义指针变量及时初始化,要么置空

悬空指针:指针freedelete之后没有及时置空=》释放操作后立即置空

C++11 nullptr 与 NULL

  • NULL:预处理变量,是一个宏,它的值是 0 ,定义在头文件<cstdlib>中,即#define NULL 0
  • nullptr:C++11中的关键字,是一种特殊类型的字面值,可以被转换成任意的其他指针类型。

nullptr的优势:

  • 有类型,类型typedef decltype(nullptr) nullptr_t;使用nullptr提高代码的健壮性。
  • 函数重载:因为NULL本质上是0,在函数调用过程中,若出现函数重载并且传递的实参是NULL,可能会出现不知和哪一个函数匹配的情况;但是传递实参nullptr就不会出现这种情况。
#include <iostream>
#include <cstring>
using namespace std;

void fun(char const *p)
{
    cout << "fun(char const *p)" << endl;
}

void fun(int tmp)
{
    cout << "fun(int tmp)" << endl;
}

int main()
{
    fun(nullptr); // fun(char const *p)
    /*
    fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous
    */
    return 0;
}

指针和引用的区别

  • 指针所指向的内存空间在程序运行的过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
  • 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占有内存空间(是否占内存)
  • 指针可以为空,但是引用必须绑定对象。(是否可为空)
  • 指针可以有多级,但是引用只能有一级。(是否能为多级)

常量指针和指针常量的区别

常量指针

常量指针本质上是一个指针,只不过这个指针指向的对象是常量。

特点const的位置在指针声明运算符*的左侧。只要const位于*的左侧,无论它在类型名的左边或者右边,都表示指向常量的指针。(可以这样理解,*左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针)。

const int* p;
int const* p;

注意1:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。

#include <iostream>
using namespace std;

int main()
{
    const int c_var = 8;
    const int *p = &c_var; 
    *p = 6;            // error: assignment of read-only location '* p'
    return 0;
}

注意2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此,可以被重新赋值。

#include <iostream>
using namespace std;

int main()
{
    const int c_var1 = 8;
    const int c_var2 = 8;
    const int *p = &c_var1; 
    p = &c_var2;
    return 0;
}

指针常量

指针常量的本质上是个常量,只不过这个常量的值是一个指针。

特点:const位于指针声明操作符右侧,表明该对象本身是一个常量,*左侧表示该指针指向的类型,即以*为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。

const int var;
int* const c_p = &var;

注意1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。

#include <iostream>
using namespace std;

int main()
{
	int var, var1;
    int* const c_p = &var;
    c_p = &var1; //error
    return 0;
}

注意2:指针的内容可以改变。

#include <iostream>
using namespace std;

int main()
{
    int var = 3;
    int* const c_p = &var;
    *c_p = 12;
    return 0;
}

函数指针和指针函数的区别

指针函数:

指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。

#include <iostream>
using namespace std;

struct type
{
    int var1;
    int var2;
};

type* fun(int tmp1, int tmp2)
{
    Type* t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}

int main()
{
    Type* p = fun(5, 6);
    return 0;
}

函数指针:

函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。

#include <iostream>
using namespace std;

int fun1(int tmp1, int tmp2)
{
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
  return tmp1 / tmp2;
}

int main()
{
  int (*fun)(int x, int y); 
  fun = fun1;
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  return 0;
}
/*
运行结果:
75
3
*/

函数指针和指针函数的区别:

  • 本质不同

    • 指针函数本质是一个函数,其返回值为指针。
    • 函数指针本质是一个指针变量,其指向一个函数。
  • 定义形式不同

    • 指针函数:int* fun(int tmp1, int tmp2);,这里*表示函数的返回值类型是指针类型。
    • 函数指针:int (*fun)(int tmp1, int tmp2);, 这里*表示变量本身是指针类型。
  • 用法不同

include " "和 < >的区别

include <文件名>#include "文件名"的区别:

  • 查找文件的位置:include <文件名>在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在的目录下查找;#include "文件名"在当前源文件所在目录中进行查找,如果没有,再到系统目录中查找。
  • 使用习惯:对于标准库中的头文件常用#include <文件名>,对于自己定义的头文件,常用#include "文件"

强制类型转换有哪几种?

  • static_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。

    • 用于基本数据类型的转换。
    • 用于类层次之间的基类和派生类之间的指针或者引用的转换(不要求必须包含虚函数,但是必须有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的额,最好用dynamic_cast进行下行转换。
    • 可以将空指针转化称目标类型的空指针。
    • 可以将任何类型的表达式转化成void类型。
  • const_cast:强制去掉常量属性,不能用于去掉变量的常量性。只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。

  • reinterpret_cast:改变指针或引用的类型,将指针或引用转换为一个足够长度的整型,将整型转化为指针或引用类型

  • dynamic_cast:

    • 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查
    • 只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回NULL;不能用于基本数据类型的转换
    • 在向上进行转换时,即派生类的指针转换成基类的指针和static_cast效果是一样的。(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。

参数传递时,值传递,引用传递,指针传递的区别

参数传递的三种方式:

  • 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
  • 指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
  • 引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。
#include <iostream>
using namespace std;

void fun1(int tmp){ // 值传递
    cout << &tmp << endl;
}

void fun2(int * tmp){ // 指针传递
    cout << tmp << endl;
}

void fun3(int &tmp){ // 引用传递
    cout << &tmp << endl;
}

int main()
{
    int var = 5;
    cout << "var 在主函数中的地址:" << &var << endl;

    cout << "var 值传递时的地址:";
    fun1(var);

    cout << "var 指针传递时的地址:";
    fun2(&var);

    cout << "var 引用传递时的地址:";
    fun3(var);
    return 0;
}
/*
var 在主函数中的地址: 009DFD7C
var 值传递时的地址:009DFCA8
var 指针传递时的地址:009DFD7C
var 引用传递时的地址:009DFD7C
*/

说明:从上述代码的运行结果可以看出,只有在值传递时,形参和实参的地址不一样,在函数体内操作的不是变量本身。引用传递和指针传递,在函数体内操作的是变量本身。

如何判断结构体是否相等?能否用memcmp函数判断结构体相等?

需要重载操作符 ==判断两个结构体是否相关,不能用 memcmp来判断两个结构体是否相等,因为 memcmp函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。

利用运算符重载来实现结构体对象的比较:

#include <iostream>
using namespace std;

struct A{
    char c;
    int val;
    A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}
    
    friend bool operator==(const A& tmp1, const A& tmp2);  //友元
};

bool operator==(const A& tmp1, const A& tmp2)
{
    return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);
}

int main()
{
	A ex1('a', 90), ex2('b', 80);
    if(ex1 == ex2)
        cout << "ex1 == ex2" << endl;
    else
        cout << "ex1 != ex2" << endl;
    return 0;
}

什么是模板?如何实现?

模板: 创建类或函数的蓝图或者公式,分为函数模板和类模板。

实现方式:模板定义以关键字template开始,后跟一个模板参数列表。

  • 模板参数列表不能为空;
  • 模板类型参数前必须使用关键字class或者typename,在模板参数列表中这个两个关键字含义相同,可互换使用。
template <typename T, typename U, ...>

函数模板: 通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

  • 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或者类型转换。
  • 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。
#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

类模板: 类似函数模板,类模板以关键字template开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。

模板的优点和缺点

模板的某些用途,例如max()函数,以前由类似函数的预处理宏来处理。

// a max() macro
#define max(a, b) ((a) < (b) ? (b) : (a))

宏和模板都在编译时进行扩展,宏总是内联扩展,当编译器认为合适时,模板也可以扩展为内联函数,因此,类似函数的宏和函数模板都没有运行时开销。

模板通常被认为是对宏的改进,模板是类型安全的。模板避免了在使用宏时遇到的一些错误。因此,模板比宏更适合处理大的问题。

使用模板存在三个主要缺点。首先,许多编译器对模板的支持非常差,因此使用模板时会导致代码的可移植性较差;其次,当在模板代码中检测到错误时,几乎所有的编译器都会产生令人困惑的错误消息;第三,每次使用模板都可能导致编译器生成额外的代码,因此不加区别的使用模板会导致代码膨胀,从而导致可执行文件过大

使用模板最大的优势在于,一个复杂的算法可以拥有一个简单的接口,编译器可以根据参数的类型选择正确的实现。

优点

  • 在多种类型但是相同代码重复的情形下使用模板,可以减少重复代码。
  • 可以使用类模板开发一组类型安全的类。
  • 使用模板可以减少开发时间。
  • 因为模板的参数在编译时是已知的,所以模板类更安全,可能比在运行时解析的代码结构更受欢迎。
  • 某些操作很难用传统的面向对象技术实现,例如相等运算符,而模板可以很方便的实现这一功能。

缺点

  • 一些编译器对模板的支持很差,因此,模板的使用会降低代码的可移植性。
  • 许多编译器在检测到模板出错时缺乏明确的说明,增加了模板开发的工作量。
  • 对模板进行调试时可能很困难,由于编译器替换了模板,所以调试器很难在运行时定位代码。
  • 模板位于头文件中,当对代码进行更改时需要完全重建所有项目。
  • 模板没有信息隐藏,所有代码都暴露在头文件中。

函数模板和类模板的区别

  • 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。
  • 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。
  • 默认参数:类模板在模板参数列表中可以有默认参数。
  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
  • 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。

模板声明与定义的问题

模板编译

当编译器遇到一个模板定义时,并不生成代码,只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才会生成代码

通常,当我们调用一个函数时,编译器只需要掌握函数的声明,类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现,因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。

模板不同:为了生成一个实例化的版本,编译器需要掌握函数模板或类模板成员函数的定义,因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义

函数模板和类模板成员函数的定义通常放在头文件中

模板和头文件

模板包含两种名字

  • 那些不依赖于模板参数的名字;
  • 那些依赖于模板参数的名字;

当使用模板时,所有不依赖于模板参数的名字都必须是可见的,由模板的提供者来保证,而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。

用来实例化模板的所有函数,类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。

模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明,模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件

对于下面的例子

test.h

#ifndef ALGORITHMS_TEST_H
#define ALGORITHMS_TEST_H

template <typename T>
class Foo
{
    T bar;
public:
    //void doSomething(T param) {/* do stuff using T */}
    void doSomething(T param);
};
#endif //ALGORITHMS_TEST_H

test.cpp

#include<iostream>
#include "test.h"

template <typename T>
void  Foo<T>::doSomething(T param) {
    std::cout << param << std::endl;
}

main.cpp

#include "test.h"
int main() {
    Foo<int> f;
    f.doSomething(3);
    return 0;
}

这种情况下出错

为什么模板类的声明和实现分开会出现连接问题呢?

因为需要单独编译,而且模板是实例化样式的多态性,在实例化模板时,编译器会使用给定的模板参数创建一个新类;

对于下面的例子

foo.h
    声明的接口 class MyClass<T>
foo.cpp
    定义执行 class MyClass<T>
bar.cpp
    用途MyClass<int>
  • 单独编译意味着可以独立编译bar.cppfoo.cpp,编译器在每个编译单元上完全独立地完成分析,优化和代码生成的所有艰苦工作;不需要做整个程序分析,只有链接器需要一次处理整个程序,链接器的工作大大简化了;
  • 当编译foo.cpp时,bar.cpp甚至不需要存在,但是仍然可以将foo.obar.o一起使用;
  • 实例化样式多态性表示模板MyClass<T>并不是真正的通用,他不能被编译成可以处理任何T值的代码。模板实际上是模板,类模板不是类,模板不能被编译成代码,只有实例化模板的结果可以被编译
  • 因此,在编译foo.cpp时,编译器无法通过看到bar.cpp来知道需要MyClass<int>,它可以看到模板MyClass<T>,但不能为此编译出响应的代码,并且在编译bar.cpp时,编译器可以看到它需要创建一个MyClass<int>,但是看不到模板MyClass<T>,只能在foo.h中看到其接口,不能看到具体类成员函数的具体实现等,因此无法创建它;
  • 如果foo.cpp中本身使用MyClass<int>,将在编译时会产生对应代码在foo.cpp中,因此当文件bar.o链接到文件foo.o它们可以连接并工作,可以利用这一事实,通过编写单个模板,在.cpp文件中实现一组有限的模板实例化,但是bar.cpp无法将模板作为模板实例化它的类型,它只能使用foo.cpp的作者认为提供的模板化类的已存在版本。

什么是模板特化?为什么特化?

模板特化的原因:模板并非对任何模板实参都合适,都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。

模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化。

  • 函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。
  • 类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。

特化分为全特化和偏特化:

  • 全特化:模板中的模板参数全部特例化。
  • 偏特化:模板中的模板参数之确定了一部分,剩余部分需要在编译器编译时确定。

说明:要区分下函数重载和函数模板特化

定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。

#include <iostream>
#include <cstring>

using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2)
{
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2)
{
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}

int main(int argc, char *argv[])
{
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/

模板成员函数能是虚函数吗?

模板成员函数不可以是虚函数。

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时(也就是使用模板时),编译器才会生成代码。

编译器希望在处理类的定义时就能确定这个类的虚函数表的大小。如果允许有虚成员模板函数,那么在编译时就要生成模板成员函数的代码,可是只有当我们使用模板时,编译器才能生成模板成员函数的代码,所以我们就需要提前知道程序中所有对模板成员函数的调用,这是无法办到的。

虚函数表中存放着虚函数的地址,而在模板类被实例化之前不能确定模板成员函数会被实例化多少个,所以也就不能确定虚函数表的大小,所以模板成员函数不能是虚函数。

但是,模板类中可以存在虚函数

函数指针和函数算子的区别

函数指针

函数指针指向特定内存中的可执行代码。解引用函数指针可以执行位于内存块中的代码。此外,参数可以传递给函数指针,然后传递给执行的代码块。

#include <iostream>

double add(double left, double right) {
    return left + right;
}

double multiply(double left, double right) {
    return left * right;
}

double binary_op(double left, double right, double (*f)(double, double)) {
    return (*f)(left, right);
}

int main( ) {
    double a = 5.0;
    double b = 10.0;

    std::cout << "Add: " << binary_op(a, b, add) << std::endl;
    std::cout << "Multiply: " << binary_op(a, b, multiply) << std::endl;

    return 0;
}

//输出
Add: 15
Multiply: 50

函数指针有几个缺点

  • 函数指针比Functors效率低,编译器通常会将函数指针作为原始指针进行传递,因此编译器很难内联代码。
  • 函数指针在存储状态方面不灵活。通过在函数内使用局部静态变量,函数只有一个全局状态,因此必须共享这个静态变量。此外,这个静态变量不是线程安全的,除非添加了适当的线程同步代码。因此,这样可能会导致多线程程序中的竞争状态和瓶颈。
  • 函数指针使分支预测器无法预测分支到哪里。
  • 函数指针具有固定的参数类型和数量,因此当使用带有不同参数类型的外部函数时,缺乏灵活性。
  • 如果代码中有多个函数签名,则使用函数指针不如使用函数模板。如果使用函数指针的话,这会导致语法困难且代码臃肿。

函数对象

函数对象允许类的实例对象像普通函数一样被调用。在C++中,这是通过重载operator()来实现的,使用函数对象的好处是它们是对象,因此可以包含状态。

#include <iostream>

// Abstract base class                                                                                                                                                                                                  
class BinaryFunction {
public:
  BinaryFunction() {};
  virtual double operator() (double left, double right) = 0;
};

// Add two doubles                                                                                                                                                                                                      
class Add : public BinaryFunction {
public:
  Add() {};
  virtual double operator() (double left, double right) { return left+right; }
};

// Multiply two doubles                                                                                                                                                                                                 
class Multiply : public BinaryFunction {
public:
  Multiply() {};
  virtual double operator() (double left, double right) { return left*right; }
};

double binary_op(double left, double right, BinaryFunction* bin_func) {
  return (*bin_func)(left, right);
}

int main( ) {
  double a = 5.0;
  double b = 10.0;

  BinaryFunction* pAdd = new Add();
  BinaryFunction* pMultiply = new Multiply();

  std::cout << "Add: " << binary_op(a, b, pAdd) << std::endl;
  std::cout << "Multiply: " << binary_op(a, b, pMultiply) << std::endl;

  delete pAdd;
  delete pMultiply;

  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值