C++泛型相关知识

参考南软2024秋C++高级程序设计课件generic1, generic2

C++程序的组织

头文件.h

头文件中一般放编译时常量const,各种声明,别名定义typedef,宏,内联函数,预处理等代码,并对需要向外扩展作用域的成员加上extern关键字

源文件.cc / .cpp

对应的,源文件中就要负责实现只在对应头文件中声明而没有定义的内容,比如函数定义、类成员定义等

作用域

分为:

  • 程序级:比如#include的内容
  • 文件级:比如全局变量、函数等
  • 函数级:比如函数的最外层局部变量
  • 块级:比如代码块的局部变量(for, while等)

namespace

两种使用方式:

  • 声明式declaration
  • 直接使用directive

设有名空间L:

namespace L {
int k;
void f(int);
// ...
}

声明式

using L::k;
using L::f;

k = 1;
f(k);

直接使用

using namespace L;

k = 1;
f(k);

在约束作用域方面,用namespace代替static

细节

  1. namespace可以用来给一个名空间起别名
namespace alias_L = L;
using namespace alias_L;

// ...
  1. namespace的作用域是影响全局的
using namespace L;

// L中的所有成员都将变成该文件下的全局变量
  1. namespace是开放的,即可以多次定义同一个命名空间,内容将进行合并
namespace L {
void f2();
int m;
// ...
}

using L::k;
using L::m;
using L::f2;
//...
  1. 可以嵌套
namespace L1 {
int a;
// ...
namespace L2 {
int b;
void f();
// ...
}
}
  1. 支持对其中的函数重载
namespace A {
void f(int);
void f(char);
void f(double);
}

void f();

int main() {
using A::f;
f('1');
// f(); // f的名空间被namespace A中的f覆盖,而A中没有重载版本f(), 因此编译错误
}

最常用的还是using namespace std;

编译预处理

编译预处理既便捷又危险,它与C++的作用域、类型、接口等概念格格不入,潜伏于环境中,穿透作用域

但其应用方式的确丰富,很难为其找到具有更好的结构且高效的替代品

#include

  • 使得接口定义成为可能
  • 组织源代码文件

#define

  • 定义符号常量
  • 定义类型
  • 泛型编程
  • 字符串拼接

宏定义是非常有用的

#define CONCAT(x, y) x##y // 拼接字符串x和y,直接按传入的字面文本x, y进行拼接
#define TO_STRING(x) #x // 将x直接转为字符串,按传入的x的字面文本

cout << CONCAT(abc, def); // abcdef
cout << TO_STRING(1); // 1
cout << TO_STRING(xxx); // xxx

#ifdef

  • 版本控制
  • 避免循环include

#pragma

  • 通知编译器添加一些编译参数等
#pragma Optimize ("Ofast")

更优雅的宏定义示例,可以简化重复但不好抽象的代码块:


#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
#define FOR_EACH(arr, fp) for (int i = 0; i < ARRAY_SIZE(arr); i++) {\
(fp)(arr[i]); \
}

int main() {
int arr[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
FOR_EACH(arr, [](const auto a) {
cout << a << endl;
});
}

泛型编程

C如何实现泛型编程

#include <stdio.h>
#include <stdlib.h>

// 定义通用的栈结构和操作
#define DEFINE_STACK_TYPE(TYPE) \
typedef struct { \
TYPE *data; \
size_t capacity; \
size_t size; \
} Stack_##TYPE; \
\
void Stack_##TYPE##_init(Stack_##TYPE *stack) { \
stack->capacity = 4; \
stack->size = 0; \
stack->data = (TYPE *)malloc(stack->capacity * sizeof(TYPE)); \
} \
\
void Stack_##TYPE##_push(Stack_##TYPE *stack, TYPE value) { \
if (stack->size == stack->capacity) { \
stack->capacity *= 2; \
stack->data = (TYPE *)realloc(stack->data, stack->capacity * sizeof(TYPE)); \
} \
stack->data[stack->size++] = value; \
} \
\
TYPE Stack_##TYPE##_pop(Stack_##TYPE *stack) { \
if (stack->size == 0) { \
fprintf(stderr, "Stack underflow!\n"); \
exit(EXIT_FAILURE); \
} \
return stack->data[--stack->size]; \
} \
\
TYPE Stack_##TYPE##_top(Stack_##TYPE *stack) { \
if (stack->size == 0) { \
fprintf(stderr, "Stack is empty!\n"); \
exit(EXIT_FAILURE); \
} \
return stack->data[stack->size - 1]; \
} \
\
void Stack_##TYPE##_destroy(Stack_##TYPE *stack) { \
free(stack->data); \
stack->data = NULL; \
stack->capacity = 0; \
stack->size = 0; \
}

// 定义具体的类型栈
DEFINE_STACK_TYPE(int)
DEFINE_STACK_TYPE(double)

int main() {
// 整型栈
Stack_int int_stack;
Stack_int_init(&int_stack);

Stack_int_push(&int_stack, 10);
Stack_int_push(&int_stack, 20);
Stack_int_push(&int_stack, 30);

printf("Top of int stack: %d\n", Stack_int_top(&int_stack));

while (int_stack.size > 0) {
printf("Popped from int stack: %d\n", Stack_int_pop(&int_stack));
}

Stack_int_destroy(&int_stack);

// 浮点型栈
Stack_double double_stack;
Stack_double_init(&double_stack);

Stack_double_push(&double_stack, 1.1);
Stack_double_push(&double_stack, 2.2);
Stack_double_push(&double_stack, 3.3);

printf("Top of double stack: %.1f\n", Stack_double_top(&double_stack));

while (double_stack.size > 0) {
printf("Popped from double stack: %.1f\n", Stack_double_pop(&double_stack));
}

Stack_double_destroy(&double_stack);

return 0;
}

输出

Top of int stack: 30
Popped from int stack: 30
Popped from int stack: 20
Popped from int stack: 10

缺点:

  • 代码几乎没有可读性
  • 难以调试
  • 需显式写出类型参数
  • 需要手动实例化

C++解决方案

template

模板 template

用法:在需要使用模板的地方最前面加上template <typename T>

#include <iostream>
#include <vector>
#include <stdexcept>

template <typename T>
class Stack {
private:
std::vector<T> data; // 使用 std::vector 动态存储数据

public:
// 检查栈是否为空
bool empty() const {
return data.empty();
}

// 获取栈的大小
size_t size() const {
return data.size();
}

// 压栈操作
void push(const T& value) {
data.push_back(value);
}

// 出栈操作
void pop() {
if (empty()) {
throw std::underflow_error("Stack underflow: cannot pop from an empty stack");
}
data.pop_back();
}

// 获取栈顶元素
T top() const {
if (empty()) {
throw std::underflow_error("Stack underflow: stack is empty");
}
return data.back();
}
};

int main() {
try {
// 整型栈
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);

std::cout << "Top of int stack: " << intStack.top() << "\n";
while (!intStack.empty()) {
std::cout << "Popped from int stack: " << intStack.top() << "\n";
intStack.pop();
}

// 浮点型栈
Stack<double> doubleStack;
doubleStack.push(1.1);
doubleStack.push(2.2);
doubleStack.push(3.3);

std::cout << "Top of double stack: " << doubleStack.top() << "\n";
while (!doubleStack.empty()) {
std::cout << "Popped from double stack: " << doubleStack.top() << "\n";
doubleStack.pop();
}

// 字符串栈
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");

std::cout << "Top of string stack: " << stringStack.top() << "\n";
while (!stringStack.empty()) {
std::cout << "Popped from string stack: " << stringStack.top() << "\n";
stringStack.pop();
}

} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
}

return 0;
}

输出

Top of int stack: 30
Popped from int stack: 30
Popped from int stack: 20
Popped from int stack: 10
Top of double stack: 3.3
Popped from double stack: 3.3
Popped from double stack: 2.2
Popped from double stack: 1.1
Top of string stack: World
Popped from string stack: World
Popped from string stack: Hello

编译器是如何处理模板的

示例

template <typename T, typename U>
void foo(T, U);

int main() {
foo(1, 2);
}

template <typename T, typename U>
void foo(T t, U u) {}

template<typename T, typename U> foo(T, U); => template<typename int, typename U> foo(int, U); => template<typename int, typename int> foo(int, int); => finish!

concept

C++20引入了一个新的关键字concept,用于定义模板的 约束条件,可以明确限定模板必须满足的参数要求,从而确保类型安全,提高程序的可靠性与稳定性。

示例

// 定义一个简单的 concept,用来约束只有可加的类型才能传入模板
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // T 类型必须支持 a + b 操作,且结果类型是 T
};

// 使用 concept 作为模板的约束条件
template <Addable T>
T add(T a, T b) {
return a + b;
}

int main() {
std::cout << add(1, 2) << std::endl; // 输出:3
// std::cout << add("Hello", "World") << std::endl; // 编译错误:不支持字符串的加法
}

*注:*C++20的concept特性是默认禁用的,必须在编译时显式加上-fconcepts参数才能编译成功

示例

$ g++ -std=c++20 -fconcepts main.cpp -o main

元编程

meta programing

指的是编写能够操作、生成或修改其他程序(或自身)代码的程序。简而言之,元编程是编写能够处理代码的代码。其核心概念是,程序在更高的抽象层次上工作,关注代码的结构和行为,而不仅仅是直接的逻辑

关键点

  • 代码生成:元程序可以根据某些输入或条件生成新的代码。

  • 反射:反射是程序在运行时检查和修改自身结构或行为的能力。例如,程序可以在运行时检查自身的函数、方法或变量。

  • :宏是一组在编译前生成代码的指令,用于自动化重复的任务。比如 C/C++ 中的宏允许以在编译前评估的方式编写代码。

C++中可以利用的特性:宏(定义宏来替代重复的代码,每次根据不同的传入参数动态生成类似的代码),模板,constexpr,面向对象编程等

Lambda表达式

语法

[captureList](paramList) specifiers exception -> returnType {body}其中除了[], (), {}都是可省的

  • captureList用于指定Lambda表达式内部如何访问其外部作用域中的变量,捕获方式有值(=)捕获、引用(&)捕获和混合捕获
  • paramList定义Lambda表达式的参数
  • specifiers用于指定Lambda表达式的属性,如mutable(可以修改捕获的变量)
  • exception指定是否抛出异常,若抛出抛什么
  • returnType说明返回值类型
  • body定义函数逻辑

示例

vector<int> arr = {2, 5, 1, 7, 9, 4};
sort(arr.begin(), arr.end(), [](const auto a, const auto b) {
return a < b; // 升序
});

int k = 1;
auto it = upper_bound(arr.begin(), arr.end(), k); // 默认升序有序

sort(arr.begin(), arr.end(), [](const auto a, const auto b) {
return a > b; // 降序
});
it = upper_bound(arr.begin(). arr.end(), k, [&](const auto a, const auto b) { // & 捕获当前作用域的所有变量
return a > b; // 指定有序是降序的
});

<functional>

<functional>库详见 C++面向对象编程进阶II

十个问题,为面向对象编程抛砖引玉

  1. 当类中未自定义构造函数,编译器是否会提供默认构造函数,为什么?
  2. 什么时候将构造函数、析构函数定义为private?什么时候使用友元、static成员?
  3. 为什么引入成员初始化表?为什么初始化表的执行次序只与类数据成员的定义次序相关
  4. 为什么引入拷贝构造函数、移动构造函数、=操作符重载?
  5. 为什么需要后期(动态)绑定?C++如何实现virtual
  6. 什么时候使用virtual
  7. public继承和非public继承分别意味着什么?
  8. 为什么=, (), [], ->不能全局重载
  9. 什么时候成员函数能返回&

大多数情况下都可以返回&,除去只读的情况(事实上只读可以返回一个常量引用),但以下情况需要注意:

  • 不要返回局部变量的引用,因为它们的生命周期在函数返回后结束。
  • 不要返回临时对象的非 const 引用,因为它们的生命周期非常短。
  • 返回动态分配的对象的引用 需要非常小心,确保管理好内存的释放。
  • 返回静态成员变量的引用 是安全的,但返回非静态成员变量的引用时要确保该对象仍然存在。
  1. 什么时候重载newdelete?怎么重载?