Hello,world!C++中的引用、移动语义、模板、包装类、迭代器和命名空间
前言
Hello,world!今天是2025年3月12日,这是本站的第一篇博客,也是我重新踏上自学CS之路的起点!
回想已流逝的两年半多本科时光,很多课程因为时间紧、任务重,学得像个“快餐式”程序员——知识点匆匆下肚,消化得却不咋样;ppt上的知识点背了一打,实践起来“一问三不知”。猛然发现,自己像个“半成品”,代码写得像“面条”,bug多得像“打地鼠”。
恰逢找到第一个日常实习,初尝真实的工作环境。实习之余决定重启学习,重新修炼“代码内功”。
从哪里开始呢?第一站是C++的基本语法。之前刷算法题时用过C++,每次没有类、没有封装、没有 unit test、没有 Makefile、没有 Git,唯一的优点是它确实能跑,缺点是“能跑”的补集;大三上做过一个4000行代码的Qt小项目,实现简单图像识别与参量的自动化计算(可见:https://github.com/WenLiuyi/fiber_Analysis )现已经成功打包成exe文件,可在Windows系统执行。但那过程简直像“渡劫”——装了几十个 G 的 Visual Studio,每次打开那笨重的 IDE,配置环境像“解谜游戏”,编译错误像“天书”,代码结构像“迷宫”。每次debug都像在玩“找茬”,头大得能顶个西瓜!但这些“坑”让我明白,自己的知识储备还像个“小池塘”,远远不够用。
这条路不会像“Hello, world!”那么简单,但最好的时机,就是现在!从“零”开始,走向“无穷大”!(超级感谢CS自学指南)
C++预备知识
引用和移动
引用
概述
引用是 C++ 中的一种机制,用来创建变量的别名。通过引用,多个名字可以指向同一块内存区域。引用通常用于函数参数的传递、改动数据的追踪以及提升性能等场景。 * 初始化:引用必须在声明时初始化,并且一旦绑定到一个变量后不能改变指向另一个变量。 * 参考传递(Pass by Reference):引用是一种传递方式,允许函数修改调用者的变量值。 * 性能优化:通过引用避免了数据的复制,提高了效率,尤其是在传递大数据结构时。
引用的声明
引用的声明使用的是单个 & 符号。例如: 1
2int a = 10;
int &b = a; // b 是 a 的引用
示例代码
1 |
|
C++移动语义
概述
移动语义是 C++
中的一种重要特性,旨在提高程序的性能,特别是在处理大型数据结构时。移动语义通过避免不必要的深拷贝,使得对象之间的数据转移更加高效。与传统的拷贝操作不同,移动操作通过“转移所有权”的方式,使得对象的资源能够直接从一个对象转移到另一个对象,而不是复制数据。
移动语义的核心概念在于区分 左值(lvalue) 和 右值(rvalue): *
左值(lvalue):
是指向内存中某个位置的对象,可以持久存在。 *
右值(rvalue)
是临时对象,通常用于表示临时的、不再需要的对象。 *
性能提升:使用 std::move
可以避免不必要的深拷贝。特别是当我们操作如
std::vector
、std::string
等需要大量内存的容器时,移动比拷贝要高效得多。
主要概念
std::move
:std::move
不是“移动”操作本身,而是将一个对象标记为可以被移动的状态,实际上它只是一个类型转换操作,将左值转换为右值。之后,移动构造函数或移动赋值操作符会将对象的所有权,从一个对象转移到另一个对象。- 右值引用(Rvalue Reference): 右值引用是使用
&&
语法声明的引用,它绑定到一个右值上。通过右值引用,我们可以通过移动语义避免不必要的拷贝。
示例代码
1 |
|
移动构造函数和移动赋值运算符
移动构造函数
移动构造函数用于在创建新对象时,将一个已有对象的资源从源对象“移动”到目标对象。通过 std::move,源对象的资源将被转移给新对象,而源对象的状态会被置为无效。
移动赋值运算符
移动赋值运算符用于将一个已有对象的资源从一个对象转移到另一个已存在的对象中。在此过程中,目标对象会接管源对象的资源,而源对象则失去对这些资源的所有权。
示例代码
Person
类包含3个构造函数:一个默认构造函数;一个带有右值引用参数的构造函数用于通过移动资源来初始化对象;一个移动构造函数- 移动构造函数
Person(Person &&person)
:接受一个右值引用Person &&person
,通过std::move
转移nicknames_
和age_
的所有权来创建新对象; - 移动赋值运算符
Person &operator=(Person &&other)
:将other
对象的资源转移到当前对象
1 |
|
模板
模板是C++中的一种语言特性,它允许你编写可以与多种数据类型一起工作的代码,而无需指定具体的类型。C++中既可以创建模板函数,也可以创建模板类。
模板类
- 模板类使得类能够与不同的数据类型一起工作,而不需要为每个数据类型编写不同的类实现。
基本模板类
下面是一个基本的模板类 Foo,它可以存储一个模板类型的元素,并在调用 print 函数时打印该元素的值: * T 是模板参数,表示类 Foo 可以存储任何类型的值。var_ 是存储该值的成员变量,print 函数用来打印该值。
1 | template<typename T> |
使用多个模板参数
模板类不仅支持单一模板参数,还可以支持多个模板参数。以下是一个接受两个不同类型参数的模板类 Foo2: * Foo2 类存储了两个不同类型的元素 var1_ 和 var2_,并在 print 函数中打印它们。
1 | template<typename T, typename U> |
特化模板类
C++允许为特定类型提供模板类的专门实现,这称为模板类特化。以下是一个模板类
FooSpecial
,它的 print
函数根据模板参数类型的不同执行不同的操作: *
在这个例子中,FooSpecial
是一个模板类,当它的类型是
float
时,print
函数会输出不同的信息("hello
float!"),而其他类型则按照常规输出存储的值。
1 | template<typename T> |
使用非类型模板参数
除了类型作为模板参数外,还可以使用常量(如整数)作为模板参数。以下是一个例子,其中
Bar
类接受一个整数作为模板参数: * Bar
类接受一个整数模板参数 T,并在 print_int
函数中输出该常量值。 1
2
3
4
5
6
7
8template<int T>
class Bar {
public:
Bar() {}
void print_int() {
std::cout << "print int: " << T << std::endl;
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 使用模板类型 int 实例化 Foo 类
Foo<int> a(3);
std::cout << "Calling print on Foo<int> a(3): ";
a.print();
// 使用模板类型 float 实例化 Foo 类
Foo b(3.4f);
std::cout << "Calling print on Foo b(3.4f): ";
b.print();
// 使用两个不同模板类型实例化 Foo2 类
Foo2<int, float> c(3, 3.2f);
std::cout << "Calling print on Foo2<int, float> c(3, 3.2f): ";
c.print();
// 实例化 FooSpecial 类,分别使用 int 和 float 类型
FooSpecial<int> d(5);
std::cout << "Calling print on FooSpecial<int> d(5): ";
d.print();
FooSpecial<float> e(4.5);
std::cout << "Calling print on FooSpecial<float> e(4.5): ";
e.print();
// 使用非类型模板参数实例化 Bar 类
Bar<150> f;
std::cout << "Calling print_int on Bar<150> f: ";
f.print_int();
模板函数
基本模板函数
C++模板函数的语法允许函数接受任何类型的数据作为参数,而不需要显式地定义数据类型。可以使用
template
关键字来定义模板函数: 1
2
3
4template <typename T>
T add(T a, T b) {
return a + b;
}typename T
是模板参数,表示函数可以接受任意类型的数据。
多个模板参数
模板函数可以接受多个模板参数。例如,下面的
print_two_values
函数接受两种不同类型的参数:
1
2
3
4template<typename T, typename U>
void print_two_values(T a, U b) {
std::cout << a << " and " << b << std::endl;
}
特化模板函数
C++允许为特定类型提供模板函数的专门实现,这称为模板特化。在下面的例子中,print_msg
函数通常打印“Hello world!”,但当类型为 float
时,打印不同的信息: 1
2
3
4
5
6
7
8
9
10template <typename T>
void print_msg() {
std::cout << "Hello world!\n";
}
// 针对float类型的模板特化
template <>
void print_msg<float>() {
std::cout << "print_msg called with float type!\n";
}
使用非类模板参数
模板的参数不一定非要是类型。你也可以使用常量表达式作为模板参数,例如下面的
add3 函数,它根据传入的布尔值决定如何修改参数: 1
2
3
4
5
6
7template <bool T>
int add3(int a) {
if (T) {
return a + 3;
}
return a;
}
调用模板函数
下面是如何调用模板函数的示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 调用 add<int> 和 add<float>
std::cout << "Printing add<int>(3, 5): " << add<int>(3, 5) << std::endl;
std::cout << "Printing add<float>(2.8, 3.7): " << add<float>(2.8, 3.7) << std::endl;
// 类型推断(函数根据传入的参数类型推断模板类型)
std::cout << "Printing add(3, 5): " << add(3, 5) << std::endl;
// 调用 print_two_values
std::cout << "Printing print_two_values<int, float>(3, 3.2): ";
print_two_values<int, float>(3, 3.2);
// 调用 print_msg
std::cout << "Calling print_msg<int>(): ";
print_msg<int>();
std::cout << "Calling print_msg<float>(): ";
print_msg<float>();
// 调用 add3
std::cout << "Printing add3<true>(3): " << add3<true>(3) << std::endl;
std::cout << "Printing add3<false>(3): " << add3<false>(3) << std::endl;
包装类
在C++中,包装类是一种管理资源的类。资源可以是内存、文件句柄、网络连接等。包装类通常采用RAII(Resource Acquisition Is Initialization,资源获取即初始化)编程技巧,这意味着资源的生命周期与类实例的生命周期绑定。当包装类的实例被构造时,它会获取相应的资源;而当实例被销毁时,资源会被释放。
IntPtrManager
实现
构造函数
1 | IntPtrManager() { |
析构函数
- 析构函数通过
delete
释放ptr_
指向的内存。为了防止在移动语义中出现悬挂指针,它会检查ptr_
是否为nullptr
。1
2
3
4
5~IntPtrManager() {
if (ptr_) {
delete ptr_; // 释放资源
}
}
移动构造函数和移动赋值操作符
- 由于包装类通常不允许复制,因为复制可能会导致双重删除资源(即两个对象管理同一资源),因此:删除了拷贝构造函数和拷贝赋值操作符,并实现了移动构造函数和移动赋值操作符。
- 移动构造函数:
1
2
3
4IntPtrManager(IntPtrManager&& other) {
ptr_ = other.ptr_; // 转移资源
other.ptr_ = nullptr; // 设置源对象为无效状态
} - 移动赋值操作符:
1
2
3
4
5
6
7
8
9
10
11IntPtrManager &operator=(IntPtrManager&& other) {
if (ptr_ == other.ptr_) {
return *this;
}
if (ptr_) {
delete ptr_; // 释放当前资源
}
ptr_ = other.ptr_; // 转移资源
other.ptr_ = nullptr; // 设置源对象为无效状态
return *this;
}
删除拷贝构造函数和拷贝赋值操作符
- 为了避免两个对象管理同一资源,IntPtrManager
类的拷贝构造函数和拷贝赋值操作符被删除:
1
2IntPtrManager(const IntPtrManager&) = delete; // 禁止拷贝构造
IntPtrManager& operator=(const IntPtrManager&) = delete; // 禁止拷贝赋值
迭代器(Iterator)
C++ 迭代器是指向容器中元素的对象,可以用来遍历容器中的元素。迭代器在 C++ STL 中被广泛使用,通常用来访问和修改容器中的元素。指针就是一种常见的迭代器,它可以用来遍历 C 风格数组。
基本操作
- **解引用运算符(*)**:返回当前迭代器指向元素的值;
- 自增运算符(++):将迭代器指向下一个元素。 STL 中的容器(如 vector, set, unordered_map)都支持迭代器。
自定义迭代器
实现自定义双向链表(DLL)迭代器的示例。 1.
双向链表节点(Node
) 定义一个节点结构体
Node,它包含指向前一个节点和后一个节点的指针,以及存储的值。
1
2
3
4
5
6
7struct Node {
Node(int val)
: next_(nullptr), prev_(nullptr), value_(val) {}
Node* next_;
Node* prev_;
int value_;
};DLLIterator
)
实现自定义迭代器,用于遍历双向链表。该迭代器类实现了以下操作符:
- 前缀自增运算符
++iter
:将迭代器指向下一个节点。 - 后缀自增运算符
iter++
:类似于前缀自增运算符,但返回值是递增前的迭代器。 - 等于运算符
==
和不等于运算符!=
:判断两个迭代器是否指向同一个节点。 - 解引用运算符
*
:返回当前节点的值。
1 | class DLLIterator { |
- 双向链表(
DLL
)
DLL
类实现了一个基本的双向链表,并且提供了Begin
和End
函数返回迭代器,用于遍历链表。Begin()
返回指向链表头部的迭代器。End()
返回指向链表末尾之后位置的迭代器(即nullptr
)。
1 | class DLL { |
- 使用迭代器遍历双向链表
- 在
main
函数中,我们演示了如何使用自定义的双向链表迭代器来遍历链表。- 插入元素:通过
InsertAtHead
插入元素。 - 遍历链表:通过前缀和后缀自增运算符来遍历链表。
- 插入元素:通过
1 | int main() { |
命名空间
命名空间(namespace) 用于将标识符(如函数、类型、变量等)组织成逻辑上的组,并避免不同标识符之间的命名冲突。命名空间通过限定作用域,防止命名冲突。例如,C++ 标准库使用 std 命名空间,因此我们通过 std::cout 来访问输出流对象 cout。:: 操作符用于指定作用域,帮助区分不同命名空间中的标识符。
函数调用
- 若在同一命名空间中调用,可以直接使用函数名;而如果在其他命名空间中调用,需要通过作用域解析运算符
::
指定完整的命名空间路径。1
2
3
4
5
6
7
8namespace ABC {
namespace DEF {
void uses_spam(int a) {
std::cout << "Hello from uses_spam: ";
ABC::spam(a); // 必须通过 ABC::spam 来调用
}
}
}
命名空间冲突
- 如果多个命名空间中有相同名字的函数或变量,它们依然可以共存,因为它们的全名(即带有命名空间的名字)是不同的。
using 关键字的使用
using
关键字可以将命名空间或命名空间中的特定成员引入当前作用域。它有两个常见用法:
*
引入整个命名空间:使得命名空间中的所有成员在当前作用域内可以直接使用,而不需要指定命名空间名。
*
引入特定成员:仅将命名空间中的某个成员引入当前作用域。
1
2using namespace B; // 引入整个 B 命名空间
using C::eggs; // 引入 C 命名空间中的 eggs 函数