C++ Day13 其他语法B-模板(泛型)

407 阅读4分钟
1. 为什么需要使用泛型?怎么样的代码不使用泛型会显得比较臃肿?
  • 泛型,是一种将类型参数化以达到代码复用的技术,C++ 中使用模板来实现泛型
  • 下面的代码不使用泛型会显得比较臃肿?
// 我们要对三种数据类型进行加法运算,我们需要分别为三种数据类型设计三个 add 方法,才能实现我们的需求
// 其实这三个方法,出了类型 int、double、Point 不一致之外,其他代码逻辑都是相同的
int add(int a, int b) {
 return a + b;
}

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

Point add(Point p1, Point p2) {
 return p1 + p2;
}


int main() {
 add(10, 20);
 add(1.2, 1.3);
 
 Point p1(10, 20);
 Point p2(100, 200);
 add(p1, p2);
 
 return 0;
}
2. C++中如何定义泛型?如何使用泛型技术改造上面的代码?
  • C++定义泛型的格式如下
template <class T> //或
template <typename T>
  • 使用泛型技术,优化上面的代码
// 使用泛型技术
template <typename T>
T add(T a,T b) {
 return a + b;
}

int main() {
 add(10, 20);
 add(1.2, 1.3);
 
 Point p1(10, 20);
 Point p2(100, 200);
 add(p1, p2);
 
 return 0;
}
3. 泛型真的让三个函数变成了一个函数吗? 对不使用泛型技术的 add 函数汇编分析?对使用泛型技术的 add 函数汇编分析?
  • 泛型并不是将三个函数变成一个函数
  • 下面的是不使用泛型技术的 add 函数汇编分析:

image.png

image.png

  • 下面的是使用泛型技术的 add 函数汇编分析:

image.png

image.png

  • 结论:泛型是编译器特性,会在编译时期生成对应的类型的函数。
4. 请问add.h、add.cpp、main.cpp,这三个文件,哪些文件会参与编译?简述 .cpp 文件变成可执行文件的过程?
  • 只有 .cpp 文件会参与编译.h 文件在预处理阶段,就原封不动的拷贝到 .cpp 文件中了
  • .cpp 文件变成可执行文件的过程主要分如下 4 个阶段
  • 预处理:将处理一些预编译指令,比如 #define #include
  • 编译:对代码进行语法分析,词法分析,语义分析等等,最终生成汇编代码
  • 汇编:汇编代码转成机器码(前面的三个阶段都是单独对每个 .cpp 文件进行的,一个 .cpp 生成一个 .obj)
  • 链接:将多个目标文件 .obj,链接生成一个可执行文件(.obj 中很多 call 的地址都是不正确的,因为是单独编译,链接会处理一些函数地址修正等等问题)
5. 为什么模板的声明和实现如果分离到 .h 和 .cpp 文件中,会导致链接错误?如何解决?
  • ①因为 main.cpp 和 add.cpp 是分开来编译的,编译器在编译 add.cpp 的模板函数式,并不知道要生成何种类型的 add 函数,所以它一个函数都不会生成
  • ②在链接阶段,main.cpp 中 add 方法调用,需要修复调用地址,这时候才发现并没有对应的方法实现,所以错误出现在链接阶段
  • ③常见链接错误如下: image.png
  • 如何解决:一般创建一个 .hpp 文件,将模板的声明和实现都放在 .hpp 文件中。
6. 使用 C++ 实现一个 int型数组类,有size()、add()、get(int index)、remove(int index)等方法
class Array {
 int m_size;
 int m_capacity;
 int* m_data;
 void rangeCheck(int index) {
     if (index >= m_size || index < 0) {
         throw "数组下标越界了";
     }
 }
public:
 /// 构造函数
 Array(int capacity = 10) {
     m_capacity = capacity > 10 ? capacity : 10;
     m_data = new int[m_capacity]();
 }
 /// 获取数组的大小
 int size() {
     return m_size;
 }
 /// 向尾部添加元素
 void add(int value) {
     if (m_size >= m_capacity) {
         throw "数组容量不足";
     }
     m_data[m_size] = value;
     m_size++;
 }
 /// 获取指定下标的元素
 int get(int index) {
     rangeCheck(index);
     
     return m_data[index];
 }
 /// 删除指定下标的元素,并返回被删除的元素
 int remove(int index) {
     rangeCheck(index);
     
     int oldElement = m_data[index];
     for (int i = index; i < m_size - 1; i++) {
         m_data[i] = m_data[i + 1];
     }
     m_size--;
     return oldElement;
 }
 ///析构函数,需要去释放内部 new 出来的对象内存
 ~Array() {
     delete m_data;
 }
};
7. 如果数组不仅仅需要存储 int,还需要存 double、Point 等等其他类型,要怎么办?
  • 使用(模板)泛型
template <class Element>
class Array {
 int m_size;
 int m_capacity;
 Element* m_data;
 void rangeCheck(int index) {
     if (index >= m_size || index < 0) {
         throw "数组下标越界了";
     }
 }
public:
 /// 构造函数
 Array(int capacity = 10) {
     m_capacity = capacity > 10 ? capacity : 10;
     m_data = new Element[m_capacity]();
 }
 /// 获取数组的大小
 int size() {
     return m_size;
 }
 /// 向尾部添加元素
 void add(Element value) {
     if (m_size >= m_capacity) {
         throw "数组容量不足";
     }
     m_data[m_size] = value;
     m_size++;
 }
 /// 获取指定下标的元素
 Element get(int index) {
     rangeCheck(index);
     
     return m_data[index];
 }
 /// 删除指定下标的元素,并返回被删除的元素
 Element remove(int index) {
     rangeCheck(index);
     
     Element oldElement = m_data[index];
     for (int i = index; i < m_size - 1; i++) {
         m_data[i] = m_data[i + 1];
     }
     m_size--;
     return oldElement;
 }
 ///析构函数,需要去释放内部 new 出来的对象内存
 ~Array() {
     delete m_data;
 }
};
8. 如果需要把 Array 作为第三方库给别人使用,需要进行声明和实现分离,要怎么做?
  • 抽离到 Array.hpp 文件中
#ifndef Array_hpp
#define Array_hpp

#include <stdio.h>

template <class Element>
class Array {
 int m_size;
 int m_capacity;
 Element* m_data;
 void rangeCheck(int index);
public:
 /// 构造函数
 Array(int capacity = 10);
 /// 获取数组的大小
 int size();
 /// 向尾部添加元素
 void add(Element value);
 /// 获取指定下标的元素
 Element get(int index);
 /// 删除指定下标的元素,并返回被删除的元素
 Element remove(int index);
 ///析构函数,需要去释放内部 new 出来的对象内存
 ~Array();
};


/// 下标范围检查
template <class Element>
void Array<Element>::rangeCheck(int index) {
 if (index >= m_size || index < 0) {
     throw "数组下标越界了";
 }
}

/// 构造函数
template <class Element>
Array<Element>::Array(int capacity) {
 m_capacity = capacity > 10 ? capacity : 10;
 m_data = new Element[m_capacity]();
}
/// 获取数组的大小
template <class Element>
int Array<Element>::size() {
 return m_size;
}
/// 向尾部添加元素
template <class Element>
void Array<Element>::add(Element value) {
 if (m_size >= m_capacity) {
     throw "数组容量不足";
 }
 m_data[m_size] = value;
 m_size++;
}
/// 获取指定下标的元素
template <class Element>
Element Array<Element>::get(int index) {
 rangeCheck(index);
 
 return m_data[index];
}
/// 删除指定下标的元素,并返回被删除的元素
template <class Element>
Element Array<Element>::remove(int index) {
 rangeCheck(index);
 
 Element oldElement = m_data[index];
 for (int i = index; i < m_size - 1; i++) {
     m_data[i] = m_data[i + 1];
 }
 m_size--;
 return oldElement;
}
///析构函数,需要去释放内部 new 出来的对象内存
template <class Element>
Array<Element>::~Array() {
 delete m_data;
}

#endif

9. 如果要让 Array 能够使用 array[2],这种下标语法,要怎么办?
  • 对下标运算符 [] 进行重载
/// 重载 [] 支持下标去值
template <class Element>
Element Array<Element>::operator[](int index) {
 return get(index);
}
10. 如果要让 Array 能够支持 cout << array <<endl 直接打印数组内容,要怎么办?
  • << 运算符进行重载
/// 重载 << 支持打印
friend ostream &operator<<(ostream &cout, const Array<Element> &array) {
 cout << "[";

 for (int i = 0; i < array.m_size; i++) {
     if (i != 0) {
         cout << ", ";
     }
     cout << array.m_data[i];
 }

 return cout << "]";
}