static

概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化。

特性

  1. 静态成员为所有类对象所共享,不属于某个具体的实例
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
class A {
public:
    A() 
    { 
        ++_scount; 
    }
    A(const A& t) 
    { 
        ++_scount; 
    }
    static int GetACount() 
    { 
        return _scount; 
    }
private:
    static int _scount;
};

int A ::_scount = 0;

void main()
{
    cout << A::GetACount() << endl; // 0
    A a1, a2;
    A a3(a1);
    cout << A::GetACount() << endl;  // 3
}

注意点

  1. 静态成员函数不可以调非静态成员函数。(因为前置没有 this 指针)
  2. 非静态成员函数可以调静态成员函数。(因为后者不需要 this 指针)

new / delete

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

new

int main()
{
	int *p1 = (int*)malloc(sizeof(int) * 10);  //C/C++ 中都是开辟10个空间
	
	int *p2 = new int(4);   //开辟一个空间并且初始化为 4
	
	int* p3 = new int[10];   //开辟10个空间  注意:int* p3 = new int[10](4)  这种是不可以的
  //不过数段短的话也可以这样初始化 
    1.int *p = new int[10]{} 或者 int *p = new int[10]() //这两种都是初始化为0
    2.int *p = new int[10]{1,2,3,4,5,6,7,8,9,10} //这种是直接赋值,但是数组太长的话还是的用for去赋值
    3.int* p = new int[10]{ 1 }; // 这种赋值的结果是 1,0,0,0,0,0,0,0,0,0
   
	A *p4 = new A;  //开一个 A类 类型的指针	
	//如果 A 类存在自定义的构造函数,可以直接在开辟空间的时候初始化成员变量
	A *p5 = new A();
	
	格式:  所需开辟空间的类型 *指针 = new 申请对象的类型
}

new 和 malloc 的区别

  1. new 不仅会开辟空间还会调用构造函数,所以 new == malloc + 构造函数 + 失败抛异常
  2. new 失败后抛出异常,malloc 失败后返回 0
  3. new 是一个操作符,malloc 是一个函数

delete

int main()
{
	int *p1 = (int*)malloc(sizeof(int) * 10);
	free(p1); // 释放 p1 所指向的空间
	
	int *p2 = new int(4);
	delete p2;   // 释放 p2 所指向的空间
	
	int* p3 = new int[10];
	delete []p3;  //释放 p3 所指向的 10 个空间
}

delete 和 free 的区别

  1. delete 会调用析构函数进行清理,而 free 不会。
  2. delete 是一个操作符,free 是一个函数。

函数模板

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  1. 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

函数模板格式

template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表) {}

// 这三个函数进行重载的时候过于繁琐,函数基本相同,出现了代码冗余
void Swap(int& left, int& right) 
{
    int temp = left;
    left = right;
    right = temp;
}
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}
void Swap(char& left, char& right) 
{
    char temp = left;
    left = right;
    right = temp;
}

使用模板来进行修改

//template<typename T> 也可以,但是 calss 用的较多
template<class T>
void Swap(T& a, T& b)  // 它只是一个函数模板,根据不同的参数类型会生成不同的函数
{
	T x = a;
	a = b;
	b = x;  // 不会编译这个函数,所以即使整个函数不加 ;也能够成功编译,但是基本的框架必须要规范
}

注意点

  1. 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
  2. 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

函数模板的实例化

1.隐式实例化

template<class T> 
T Add(T left, T right) 
{
    return left + right;
}
int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.0, d2 = 20.0;
    Add(a1, a2);
    Add(d1, d2);

    /*
    该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
    通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
    编译器无法确定此处到底该将T确定为int 或者 double类型而报错
    注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
    Add(a1, d1);
    */

    // 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
    Add(a, (int)d);
    return 0;
}

2.显示实例化

template<class T> 
T Add(T left, T right) 
{
    return left + right;
}
int main(void) {
    int a = 10;
    double b = 20.0;

    // 显式实例化
    Add<int>(a, b);
    //如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
    return 0;
}

3.非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

namespace bite
{
    // 定义一个模板类型的静态数组
    template<class T, size_t N = 10>
    class array
    {
    public:
        T& operator[](size_t index) { return _array[index]; }
        const T& operator[](size_t index)const { return _array[index]; }

        size_t size()const { return _size; }
        bool empty()const { return 0 == _size; }

    private:
        T _array[N];
        size_t _size;
    };
}

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  2. 非类型的模板参数必须在编译期就能确认结果。

模板的特化

函数模板的特化步骤

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool IsEqual(T& left, T& right)  //这个情况要是传string就是出错,因为string没有重载比较
{	
	return left > right;
}

template<>  //使用特化模板,当输入string时就会自动调用这个函数,而其他时候会调用上面的
bool IsEqual<string>(string& left, string& right) 
{
    if (strcmp(left.c_str(), right.c_str()) > 0)
        return true;

    return false;
}

类模板特化

1.全特化

  • 全特化即是将模板参数列表中所有的参数都确定化。
template<class T1, class T2>
class Data
{
public:
    Data() { cout << "Data<T1, T2>" << endl; }
private:
    T1 _d1;
    T2 _d2;
};
template<>
class Data<int, char> {
public:
    Data() { cout << "Data<int, char>" << endl; }
private:
    T1 _d1;
    T2 _d2;
};
void TestVector()
{
    Data<int, int> d1;
    Data<int, char> d2;
}

2.偏特化

  • 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。而它又有两种形式

<1> 部分特化

// 将第二个参数特化为int
template <class T1>
class Data<T1, int> {
public:
    Data() { cout << "Data<T1, int>" << endl; }
private:
    T1 _d1;
    int _d2;
};

<2> 参数更进一步的限制

  • 偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
    Data() { cout << "Data<T1*, T2*>" << endl; }

private:
    T1 _d1;
    T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
    Data(const T1& d1, const T2& d2)
        : _d1(d1)
        , _d2(d2) 
    {
    cout << "Data<T1&, T2&>" << endl;
    }

private:
    const T1& _d1;
    const T2& _d2;
};
void test2()
{
    Data<double, int> d1; // 调用特化的int版本
    Data<int, double> d2; // 调用基础的模板 
    Data<int*, int*> d3; // 调用特化的指针版本
    Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

模板分离编译

什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

以一下代码为模板分析

// a.h
template<class T> T Add(const T& left, const T& right);
// a.cpp
template<class T> T Add(const T& left, const T& right) {
    return left + right;
}
// main.cpp
#include"a.h"
int main()
{
    Add(1, 2);
    Add(1.0, 2.0);

    return 0;
}


解决方法

  1. 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。
  2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用

模板的缺陷

  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

更多推荐

C++ static 成员、new / delete /特化模板/模板分离编译的详解