C++介绍

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

C++历史

今年不考,以后应该也不会考了

C++的历史:1979-1991

在真实世界中为其进化一门语言:1991-2006

改变世界:2006-至今

C和C++的关系

  1. C++完全包含C语言成分,C++支持C所支持的全部编程技巧(C的超集);同时C++还添加了OOP支持
  2. 任何C程序都能被C++用基本相同的方法编写,并具备相同的运行效率和空间
  3. C++还引入了重载、内联函数、异常处理等功能,对C中的过程化控制及其功能进行了扩充
  4. C++由以下4部分有机组成
    • C
    • OOP
    • STL
    • Inside-Model

语言的类别

C/C++是静态强类型语言,C++比C”更“静态

结构化编程

程序 = 数据结构 + 算法

ADT

Abstract Data Type 抽象数据类型

结构化编程需要关注抽象数据类型、联系、优先级、类型转换(强制、隐式、溢出?)、求值顺序、副作用

cast

此处介绍各种类型的cast(类型转换)

static_cast

用法:static_cast<>(),效果类似于直接用(类型)的强制类型转换

const_cast

用法:const_cast<>(),用于去除const变量的修饰符const

const int c = 10;
int* p = const_cast<int*>(&c);
*p = 20;
cout << &c << ": " << c << endl;
cout << p << ": " << *p << endl;

输出

0012FF74 10
0012FF74 20

个人看法:在编译器眼中,c是一个编译时常量,可能维护了一个类似常量表的东西,凡是遇到读c的地方统统换成这个对应的常量,因为const类型的变量不允许修改,因此在编译器眼中,这段代码cout << &c << ": " << c << endl;实际上是cout << &c << ": " << 10 << endl;;而p是一个指向c的地址的指针,它的的确确把这个内存中的值改了,const只是保证了c这个变量不被修改,这个变量只是用来解释一个特定内存中存储的二进制序列,并不保证内存一定不会被修改。那么编译器保证变量c始终是个常量的做法应该就是维护一个常量表来做替换。

reinterpret_cast

用法:reinterpret_cast<>()

reinterpret_cast 是 C++ 中的一个强制类型转换操作符,用于在不同类型之间进行转换,尤其是指针和整数之间的转换。它是 C++ 中最强大、最危险的类型转换操作符之一。使用 reinterpret_cast 可以实现低级别的内存操作,绕过类型系统的类型检查。

// float -> int
float f = 1.45;
int* x = reinterpret_cast<int*>(&f);
cout << *x << endl; // 1
// 指针 -> 整数
uintptr_t address = reinterpret_cast<uintptr_t>(x); // 把指针变成了整数,实际上就是指针本身的数值
cout << "address of x: " << address << endl;
// 整数 -> 指针
int* y = reinterpret_cast<int*>(address); // 把整数变成了指针,实际上就是给指针本身一个值(特定内存地址)
cout << *y << endl; // 1

当然也可以在不同类型的指针之间转换,比如int*char*

dynamic_cast

用法:dynamic_cast<>()

动态类型转换一般用于多态,即基类指针和派生类指针之间的转换,且基类中必须要有虚函数(virtual关键字)才能使用动态类型转换,面向对象编程中常用

auto

auto关键字可以根据值的实际类型决定变量的类型

C++为什么需要auto?

  • 减少冗余:有些类型特别长,写起来容易出错,提供auto关键字可以减轻程序员的负担,把类型判断交给编译器
  • 简化模板编程
  • 类型推导支持泛型编程
  • 支持返回类型推导(即可以把返回值声明为auto
  • 提高编程的灵活性,更优雅

但也有缺点,在大量使用auto的代码中,调试起来的可读性可能会变差

union和struct

C语言相同,只需要注意struct默认访问权限是publicclass默认是private

tuple

std::tuple 是 C++11 引入的一种数据结构,用于存储多个不同类型的元素。与 std::pair 只能存储两个元素不同,std::tuple 可以存储任意数量和类型的元素。它是一个模板类,可以存储多个不同类型的数据。

使用 std::get<index>(tuple) 来访问元组中的元素,其中 index 是元素的索引(从 0 开始)。

  • std::tuple_size结合decltype() 用于获取元组中元素的个数。
  • std::tuple_element 用于获取指定索引的元素类型。

std::tie 可以将元组的元素与一组变量进行绑定,使得你能够解构元组。常用于返回多个值的函数

std::make_tuple 是一个方便的函数,可以用来创建一个元组,并且自动推导元素类型

示例:

using namespace std;

tuple<int, double, string> t(1, 1.4, "514");
auto first = get<0>(t), second = get<1>(t), third = get<2>(t);
auto size = tuple_size<decltype(t)>::value; // 3
tuple_element<1, decltype(t)>::type _second_ = get<1>(t); // tuple_element<1, decltype(t)>::type获得第二个元素的类型,不过不如auto方便

tuple<int, double, std::string> getTuple() {
return std::make_tuple(1, 3.14, "Hello"); // 创建一个元组
}
int a;
double b;
string c;
tie(a, b, c) = getTuple();

optional

std::optional 是 C++17 引入的标准库类型,用于表示可能为空的值。它是一种封装类型,可以表示“存在值”或“无值”的状态,避免必须要用指针中的空指针代表值不存在的概念。

std::optional的默认构造函数是“无值”的;可以通过成员函数std::optional::has_value()判断是否有值

using namespace std;

optional<int> o1; // 默认无值
optional<int> o2 = 10; // 有值10
optional<int> o3 = nullopt; // 显式表达无值

cout << o1.has_value() << o2.has_value() << o3.has_value(); // 010

optional<string> getString(const char* str) {
if (str != nullptr) {
return string(str);
}
return nullopt;
}

optional<string> getString(const char* str, int length) {
if (str != nullptr) {
return string(str, length);
}
return nullopt;
}

variant

std::variant表示可以存储多种可能类型的数据,但同一时刻只能存储一种类型的值,可以通过std::getstd::visit访问存储的值

using namespace std;

variant<int, double, string> v;
v = 1;
cout << get<int>(v); // 1
v = "14";
cout << get<string>(v); // "14"
v = 5.14;
cout << get<double>(v); // 5.14

std::holds_alternative 检查当前 variant 是否存储了某种类型

cout << holds_alternative<int>(v); // 1
cout << holds_alternative<char>(v); // 0

std::visit可以访问std::variant中的元素并做相应的操作

struct Visitor {
void operator()(int i) const {
cout << "int: " << i << std::endl;
}

void operator()(double d) const {
cout << "double: " << d << std::endl;
}

void operator()(const std::string& s) const {
cout << "string: " << s << std::endl;
}
};

v = 42;
// 使用 visit 访问并打印 variant 中存储的值
visit(Visitor(), v); // 输出: int: 42
v = 3.14;
visit(Visitor(), v); // 输出: double: 3.14
v = "Hello Variant!";
visit(Visitor(), v); // 输出: string: Hello Variant!

相比传统的unionvariant的优点有:

  • 类型安全:不像传统的 unionstd::variant 是类型安全的,编译器会确保你只在正确的类型上进行操作
  • 灵活
  • 简化代码

any

类似variant但不需要提前声明存储的类型,可以存储任意类型;也可以视作是类型安全的void*(直接使用void*可能引发问题)

通过 std::any_cast<>() 来访问存储的数据

示例:类似auto

// 使用 std::any 存储不同类型的值
std::any a = 42; // 存储 int
std::any b = 3.14; // 存储 double
std::any c = std::string("Hello, Any!"); // 存储 string

// 提取并访问存储的值
std::cout << std::any_cast<int>(a) << std::endl; // 输出: 42
std::cout << std::any_cast<double>(b) << std::endl; // 输出: 3.14
std::cout << std::any_cast<std::string>(c) << std::endl; // 输出: Hello, Any!

std::any::has_value()检查 std::any 是否包含有效的值。如果 std::any 中存储了一个值,返回 true,否则返回 false

std::any::reset()清除 std::any 中存储的值,使其不再包含任何值

cout << std::a.has_value();; // 1
std::a.reset();
cout << std::a.has_value();; // 0

智能指针

RAII

Resource Acquisition Is Initialization

RAII是C++中重要的思想,即资源在初始化时分配,也可以理解成在构造函数(初始化时)中获取资源并初始化,在析构函数中释放资源(归还)。

智能指针通过多套一层来管理指针资源,减少了内存泄漏的可能性

class A {
int x;
public:
A(int a): x(a) {}
};

void old_use(int var) {
A* a = new A(var);
// ...
if (...) throw ...; // 可能忘记释放a导致泄漏
// ...
delete a; // 容易忘记导致泄露
}

void new_use(int var) {
auto p = unique_ptr<A>(new A(var)); // 利用智能指针unique_ptr而不是裸指针A*来管理new出来的指针资源
// ...
if (...) throw ...; // 不会泄露指针资源,因为退出当前函数时p的生命周期结束自动调用其析构函数,而在智能指针的析构函数中会在特定规则下主动释放资源
// ...
// 避免了潜在的内存泄漏风险
}

unique_ptr

unique_ptr独占资源,只能有一个指向内存的指针

make_unique<>()也可以创建一个独享智能指针

unique_ptr不允许拷贝但允许移动,可以通过std::move将资源的所有权从一个unique_ptr转移到另一个

auto p1 = unique_ptr<A>(new A(1));
auto p2 = make_unique<A>(2);
// 使用move转移资源
auto p3 = move(p2);

shared_ptr, weak_ptr

shared_ptr

std::shared_ptr 是一个智能指针,它允许多个指针共同拥有同一个对象的所有权。当最后一个 shared_ptr 被销毁时,它所管理的对象会自动销毁(即调用析构函数并释放内存)。它通过引用计数机制来跟踪有多少个 shared_ptr 指向同一个对象,但存在一个潜在的循环引用的问题

*循环引用问题:*两个或多个对象之间相互持有对方的引用,从而导致它们的引用计数永远不会归零

std::shared_ptr::use_count 返回 shared_ptr 指向同一个对象的引用计数

std::shared_ptr::reset使当前共享指针不再指向管理的对象,引用计数 - 1

unique_ptr一样可以通过shared_ptr<>()make_shared<>()创建共享智能指针,不同的是,shared_ptr可以直接通过赋值运算符创建一个指向同一对象的shared_ptr

using namespace std;

auto sp = shared_ptr<int>(new int(3));
auto sp1 = make_shared<int>(1);
shared_ptr<int> sp2 = sp; // sp2和sp指向同一资源
auto sp3 = sp1; // 同理,sp3和sp1指向同一资源
cout << sp.use_count() << " " << sp2.use_count() << endl; // 2 2
cout << sp1.use_count() << " " << sp3.use_count() << endl; // 2 2

weak_ptr

为了避免循环引用问题,引入weak_ptr

它本身不管理对象的生命周期,也不增加对象的引用计数weak_ptr 只是观察一个对象,但不拥有它。可以通过 std::weak_ptr 来访问 shared_ptr 指向的对象,前提是该对象仍然存在(即至少有一个 shared_ptr 在管理该对象)

使用 std::weak_ptr::lock 可以尝试从 weak_ptr 获取一个 shared_ptr,如果对象已被销毁lock()返回一个空的 shared_ptr

应用场景:避免循环引用,如果两个对象相互引用 shared_ptr,就会导致循环引用,导致内存无法释放。使用 weak_ptr 可以避免这个问题。

例如,两个对象互相持有 shared_ptr,但其中一个对象通过 weak_ptr 引用另一个对象,这样就不会导致引用计数增加,从而避免循环引用。

auto sp = make_shared<int>(1);
weak_ptr<int> wp = sp; // weak_ptr不增加引用计数
cout << sp.use_count() << endl; // 1
auto sp1 = wp.lock(); // 尝试获取一个shared_ptr,成功
if (sp1) {
cout << "sp1 success" << endl;
} else {
cout << "sp1 fail" << endl;
}
cout << sp.use_count() << endl; // 2
sp1.reset();
sp.reset(); // 引用计数归0,释放资源
auto sp2 = wp.lock(); // 没有资源被管理了,返回空shared_ptr,获取失败
if (sp2) {
cout << "sp2 success" << endl;
} else {
cout << "sp2 fail" << endl;
}

总结

特性 std::shared_ptr std::weak_ptr
引用计数 增加引用计数,管理对象的生命周期 不增加引用计数,不能管理对象生命周期
访问对象 直接访问对象 通过 lock() 转换为 shared_ptr,访问对象
对象生命周期管理 当最后一个 shared_ptr 被销毁时,自动删除对象 不管理对象生命周期,不能删除对象
避免循环引用 会导致循环引用问题 可以避免循环引用

数组

高维数组的本质:用一维数组组织,用高维数组解释

#include<bits/stdc++.h>
using namespace std;

void show(int a[], int n) {
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
}

void show(int a[][2], int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < 2; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
}

void show(int a[][2][3], int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 3; k++) {
cout << a[i][j][k] << " ";
}
cout << endl;
}
}
}

int main() {
int b[12]; // 创建一个一维数组,接下来分别按照一维、二维、三维数组来解释这个内存中组织成一维的线性数组
for (int i = 0; i < 12; i++) {
b[i] = i + 1;
}
show(b, 12);
typedef int T[2]; // 按二维数组解释 2 * 6
typedef int T1[3];
typedef T1 T2[2]; // 按三维数组解释 2 * 3 * 4
show((T*)b, 6);
show((T2*)b, 2);
}

事实上计算机中没有“维度”的概念,数据在其中并非立体,因此看似有空间感的高维数组,在实现层面都是以一维线性数组组织的。

(•‿•)