C++期末复习
根据上课 PPT 和同学与学长的笔记整理而成。
介绍
C++ 历史
先跳过。。。
结构化编程部分
强制类型转换
article link="/posts/C++拾遗/C++-强制类型转换" >}}
表达式的副作用
副作用,就是一个过程是否会修改参与的变量的性质。
在表达式中,有副作用的运算符:=
+=
++
<<
等等
无副作用的运算符:+
&&
等等
decltype
和 auto 有相似之处。
auto 和 decltype 都用在声明变量上,都是基于 RTTI 机制实现的。
auto 是对初始化的右值进行类型推导,然后给声明的变量确定类型、赋值。
decltype 是对传入的参数类型进行类型推导,然后本身就作为这个类型的名称。
int x = 0;
int& ref = x;
auto ref1 = x;
decltype(ref) ref2 = x; // equals to "int& ref2 = x"
既然本身就作为这个类型的名称,我们也可以结合 using 和 typedef:
int x = 0;
int& ref = x;
using int_ref = decltype(ref);
参考
C++11特性:decltype关键字 - melonstreet - 博客园
聚合初始化
聚合初始化的规则比较复杂,考试应该不涉及。下面简单举几个例子:
int[] arr = {1, 2, 3};
int[5] arr1 = {1, 2, 3, 4};
vector<int> arr2{1, 2, 3};
vector<vector<int>> arr3{{1, 2, 3}, {4, 5}, {6}};
Union 实现多态
父类联合体包含所有子类结构体。然后记得给子类加上类型的枚举。
例子中的声明顺序只是为了方便查看,不用在意是否能编译通过。
// Base
union FIGURE
{
FIGURE_TYPE t;
Line line;
Rectangle rect;
Ellipse ellipse;
};
// Derived
struct Line{
FIGURE_TYPE t;
int x, y;
};
struct Ellipse
{
FIGURE_TYPE t;
int x, y, r;
};
struct Rectangle{
FIGURE_TYPE t;
int left, top, right, bottom;
};
Struct 对齐
先讨论没有 #pragma pack
的情况,如何确定 struct 的对齐与大小。
首先有如下规则:
- 结构体的成员声明的顺序就是在内存中的顺序
- 某一个成员相较于 struct 开头的偏移量,必须是自身大小的整数倍
- 整个结构体大小必须是最大成员大小的整数倍
第一条规则,确定了成员的顺序就是声明的顺序。
我们按照声明的顺序,一个一个地应用第二条规则。不满足规则的,就补齐。
最后,看满不满足第三条规则。不满足就补齐。
有了 pragma 之后,每个数据成员的对齐,按照#pragma pack指定的数值和自身对齐模数中较小的那个。
cin/cout 输入/输出操纵符
操纵符是令代码能以 operator« 或 operator» 控制输入/输出流的帮助函数。
包括的一些感觉比较重要的操纵符如下 输入用:
skipws
get_time
输出用showbase
setbase
uppercase
dec
hex
oct
setprecision
setw
put_time
可以翻阅 cppreference -> 输入输出库 -> 输入输出操纵符
Tuple
大名叫 std::tuple
。元组。能保存任意数量的任意类型的成员变量。可以看成一个类型安全、使用灵活便捷的结构体。
头文件 <tuple>
tuple 是静态的,需要在声明的时候就确定要保存哪些类型:
std::tuple<int, double, std::string, char> t;
初始化:
// constructor
std::tuple<int, double, std::string, char> t(1, 2.0, "hello", 'c');
// make_tuple
t = make_tuple(1, 2.0, "hello", 'c');
使用 std::get<>
获取元素:
int first = std::get<0>(t);
int i = 1;
std::get<i>(t); // Wrong!!! Cannot use variable
使用 std::tie
解包:
int a;
double b;
std::string c;
char d;
std::tie(a, b, c, d) = t;
使用 std::tuple_size
获取 tuple 元素个数:
int el_count = std::tuple_size<decltype(t)>::value;
参考
C++ tuple(STL tuple)模板用法详解 - RioTian - 博客园
Optional
和 java 的 optional 如出一辙。大致是为了包装函数返回值而生的。
头文件 <optinal>
optional 可以理解为包含了两个信息:函数运行信息(成功,失败)、函数返回值
声明 optional:
std::optional<T> opt; // 不包含值的 optional
std::nullopt; // 不包含值的 optional,宏定义好了
std::optional<T> opt(value); // 包含 value
make_optional<T>(value); // make 方法
函数失败:
return std::nullopt;
函数成功,返回 result:
return make_optional<T>(result);
函数调用方:
使用 has_value
检查函数返回是否为空(函数执行是否失败)
如果成功,可以直接将 optional 当成结果来取值。注意,这里 value 类似迭代器,是一个指向结果的指针。
auto result = foo();
if(!result.has_value()) {
cout<< "failed";
}
else {
result->xxx ...; // 直接将 optional 对象当成返回结果的指针用
}
参考
C++17 新特性之 std::optional(上) - 知乎
C++三剑客之std::optional(一) : 使用详解_c++ optional-CSDN博客
Pair
特化的 tuple。只能放两个元素。
头文件 <utility>
用 first 和 second 来访问两个元素。
make_pair 来创建一个 pair。通过 tie 来解包。和 tuple 一毛一样。
Variant
类型安全的 union。但不允许包含引用、数组、void。
头文件 <variant>
创建与访问:
std::variant<int, float, double> var;
var = "abc"; // or var.emplace("abc")
std::get<std::string>(var); // "abc"
std::get<int>(var); // exception std::bad_variant_access
var = 0.1f;
const auto intPtr = std::get_if<float>(&var) // safe get
检查当前是否持有某类型:
使用 std::holds_alternative
if(std::holds_alternative<int>(var)) {
std::cout<<"var holds int"<<endl;
}
空状态:
即 variant 什么都没放的 “无值” 状态。用 std::monostate
表示
std::variant<std::monostate, int> var;
参考
C++17 std::variant 详解:概念、用法和实现细节 - 非法关键字 - 博客园
Any
类型安全的,能存放任意类型数据的容器(void*)。
头文件 any
声明与赋值:
std::any a;
a = 1; // store an integer
a.emplace(1); // same effect as above
a = make_any(1) // make method
取出使用:
a = 1;
std::any_cast<int>(a); // returns 1
std::any_cast<char>(a); // throw std::bad_any_cast exception
检查是否有值:
使用 has_value
成员函数。返回 bool。
std::any a;
a.has_value(); // false
a = 1;
a.has_value(); // true
检查所存储元素类型:
使用 type 成员函数。返回成员类型的 typeid
a = 1;
a.type() == typeid(int) // true
a.type() == typeid(char) // false
new/delete
在使用 new 创建数组时,前 4 个字节会存放声明的数组长度。
方便 delete[] 时,知道要释放多少空间。
RAII 与智能指针
article link="/posts/C++拾遗/杂项" >}}
C 数组特性
article link="/posts/C++拾遗/指针、引用与数组" >}}
指针作为函数参数
为什么
- 提高传输效率,不用拷贝传递的实参
- 使用函数的副作用,改变传入参数的值。如果不想要副作用,可以加 const
函数指针
没啥讲的。方便实现函数的动态调用。然后就是 lambda 函数。
函数指针的解析,看:
article link="/posts/C++拾遗/类型解释" >}}
函数执行机制
如下只是大致的步骤,会根据具体的函数调用约定而有些许不同。
- 建立被调用函数的栈空间
- 参数传递:
- 值传递 (call by value)
- 引用传递 (call by reference)
- 保存调用函数的运行状态
- 返回地址
- 调用者的 ebp
- 将控制转交被调函数
- 设置新的 ebp 和 esp
- 恢复上下文
- 恢复调用者 ebp
- 恢复返回地址
函数调用约定
函数调用约定回答了如下问题:
- 当参数个数多于一个时,按照什么顺序把参数压入堆栈
- 函数调用后,由谁来把堆栈恢复原装
stdcall
pascal 风格的调用约定。
int __stdcall foo(int a, int b);
- 参数从右向左压栈
- 函数自己恢复堆栈
例子中,被调用者 foo 的返回的汇编为
ret 8
,表示清除 a 和 b 参数占的堆栈 - 函数名的标签变为
_
+函数名
+@
+参数大小
例子中,函数的标签为_foo@8
cdecl
C 调用约定。是 C 语言默认的调用约定。
int __cdecl foo(int a, int b);
int foo(int a, int b); // default is cdecl
- 参数从右往左压栈
- 调用者恢复堆栈
被调用者直接用
ret
返回,不恢复堆栈。 调用者在call foo
之后,使用add esp, 8
来恢复两个 int 参数的堆栈。 - 函数名标签
_
+函数名
这种调用约定能支持可变参数。可变参数在声明里面是放在最右边的,这样保证了左边的参数先入栈,能通过 ebp 确定,从而确认可变参数。并且调用者可以根据自己传的参数大小来恢复堆栈。
fastcall
和 stdcall 类似,但是更 fast,因为用到了寄存器。
int __fastcall foo(int a, int b);
- 第一个和第二个声明的大小小于等于 DWORD 的参数,通过 ecx 和 edx 传 例子中,a 和 b 分别放到 ecx 和 edx 中。然后就没有参数需要压栈了。
- 其他的参数还是按 stdcall 的方式压栈
- 被调用者清理堆栈
thiscall
C++ 默认的调用约定。特殊处理了 this 指针。
int foo(int a, int b);
int __thiscall foo(int a, int b); // wrong! no need for '__thiscall'
- 参数从右向左压栈
- 参数个数确定的情况下,this 指针通过 ecx 传递;有可变参数,this 指针最后被入栈
- 参数个数确定,被调用者清理堆栈;可变参数,调用者清理堆栈
参考
C/C++ 函数调用约定(__cdecl、__stdcall、__fastcall)-CSDN博客
关于调用约定 cdecl、stdcall 和 fastcall 的区别 | 拾遗记
stdcall、cdecl、fastcall、thiscall 、naked call的汇编详解 - findumars - 博客园
格式化串攻击
由于调用约定的存在,不定参数的函数是不知道参数的个数与大小的。这在 scanf 和 printf 上会产生漏洞。
比如,用户在 scanf 输入字符串时,故意包含了格式化字符
char str[100];
scanf("%s", str); // 输入 %x\n%x\n%x\n
printf(str);
printf 只知道你传入了 str 格式串。一旦扫描到了 %x,就将对应偏移量的参数给输出出来,但是调用方并没有传对应的参数,这会导致 printf 输出对应地址上的内容。
所以,当 printf 传入参数不足格式化串中字符的个数时,就会导致这个漏洞。
- 泄露地址
- 栈上地址任意写入(通过输入 %n)
函数重载
原则:
- 名同,参数不同(个数、类型、顺序)
- 返回值类型不作为区别重载函数的依据
特殊情况:传入类型没有重载,但是可以隐式转换到多个重载的,编译无法通过
void foo(char a){}
void foo(double a){}
foo(1); // Call to 'foo' is ambiguous
默认参数
函数传入的参数可以设置默认参数,在不传默认参数时使用默认参数指定的值。就像 Python 一样。
注意点:
- 默认参数的声明,要在函数原型中给出。如果没有函数原型,就在函数定义中给出。
- 默认参数从右到左声明,不能间断
void foo(double, char, int=2); // okay
void foo(double=1.0, char, int); // wrong!!!
void foo(double=1.0, char, int=2); // wrong!!!
- 默认函数和函数重载不能出现 ambiguous
void f(int);
void f(int, int=2);
f(1); // ambiguous!!!
内联函数
inline 关键字
目的
- 提高可读性
- 提高效率,因为减少了函数调用的过程
限制
- 不能递归
- 不能作为函数指针
适用:频率高、简单、小段的代码
其他:inline 声明仅仅是请求,编译器可以拒绝
缺点
- 增大目标代码
- 换页抖动
- 降低指令 cache 的命中率
Namespace
命名空间。限制全局标识符的作用域,方便区分同名函数。
声明形式:
- declaration
using std::cout;
using std::cin;
- directive 不建议同一文件多次用 directive
using namespace std;
- 默认匿名命名空间
using ::variable;
- 别名
namespace std_alias=std;
- 全局 只要在某个文件里声明了 namespace,其他文件可以直接访问
- 开放 namespace 的内容完全公开
- 可嵌套
namespace A{
int a;
namespace B{
int b;
}
}
A::a;
A::B::b;
- 重载
编译预处理
PPT 上说:与作用域、类型、接口等概念格格不入。
能理解,但理解得不多🫠。
include
作用是复制其中的文件内容到此处。
- 包含头文件
- 防止重定义:用
#ifdef
等
- 防止重定义:用
- 替换操作
printf("#")
""
与<>
使用<>
时,预处理器会按如下顺序寻找文件:
- 到编译选项 -l 指定的目录
- 到环境变量 INCLUDE 指定的目录
使用
""
时: - Current Workspace Directory
- 编译选项 -l 目录
- 环境变量 INCLUDE 目录
define
替换文本宏。define
定义替换,undef
取消定义
- 空 define 多用于防止重复定义
- 预定义宏 比如 c++ 标准版本的宏
- 功能特性测试宏 比如开优化的宏
- 仿函数宏
- 含参数的宏,比如之前打 oi 常用的省时间宏
#define f(q, w, e) for(int q = w; q < e; q++)
f(i, 0, n) {...}
- 还能实现泛型
#define add(x, y) (x + y)
加括号是必要的,为了保证加法不受外部运算符优先级的影响。
- 对于函数体的替换 用 do while(0) 保证函数体的独立性与完整性
#define incAndPrint(a) do{\
a++;\
a.print();\
} while(0);
如果没有 do while(0),放在无花括号的 if 里就出问题了。
// with out do while(0)
if(a)
incAndPrint(a);
// equals to
if(a)
a++;
a.print(); // outside of if
#
运算符 运算符后跟参数名,将这个参数转换成字符串。
#define foo(x) #x
foo(12345); // "12345"
foo(n); // "n"
##
运算符 运算符后跟参数名,将这个参数的名字作为一个 token 拼接到代码中。
#define def_stack(x) typedef stack_##x{}
def_stack(int); // typedef stack_int{}
pragma
结构体对齐。没研究过,先跳过。
泛型
前面说过,可以用宏实现泛型:
#define CREATE_STACK(T) \
typedef struct stack_##T { \
T* array; \
int top; \
} stack_##T\
\
void __init(stack_##T *stack, int capacity) {...}\
void __push(stack_##T *stack, T data) {...}\
...
但是太麻烦了
- 代码可读性查
- 难调试
- 需要显式写出类型参数
- 手动实例化
使用模板实现:
template<typename T>
struct Stack{
T* array;
int capacity;
int top;
};
...
好写很多。
concept
约束类模板和函数模板的模板类型和非类型参数的命名要求。
比如,限制函数模板不能是指针:
template<typename T>
concept DataAvailable = !std::is_pointer<T>::value;
template <DataAvailable T>
void function(T t) {...}
其他写法:
template <typename T>
requires DataAvilable
void function(T t) {...}
template <typename T>
void function(T t) requires DataAvilable<T> {...}
void function(DataAvilable auto v) {...}
可以用 &&
组合多个约束:
```cpp
template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;
SFINAE
Substitution Failure Is Not An Error
模板的匹配失败不是错误。在匹配类型失败后,编译器还需要尝试其他的可能性
特化
参考
元编程 Meta programming / constexpr
在编译期就计算出运行时需要的东西。
面向对象部分
面向对象概念
program = Object1 + Object2 +…… + Objectn
object: data + operation
Message: function call
Class
OOP classify:
- Object-Oriented
- 没有 inbuilt objects,但有所有 OO 的特性。如继承、多态
- Object-Based
- 基于 inbuilt objects,但没有所有 OO 的特性
- Object-Oriented
构造函数
构造函数
构造函数有三种:
- 无参构造函数
- 有参构造函数
- 拷贝构造函数
默认提供
编译器会提供 默认无参构造函数 和 拷贝构造函数。
Empty();
Empty(const Empty&);
默认的无参构造函数是空函数,什么都不干。
拷贝构造函数,对所有成员进行浅拷贝。
成员初始化表
析构函数
编译器默认提供空的析构函数。
拷贝构造函数
移动构造函数
动态内存
const 成员
static 成员
友元
友元类
友元函数
友元函数不是类的成员函数!!!
编译器默认提供的函数
class Empty {
Empty();
Empty(const Empty&);
Empty(const Empty&&); // after C++ 11
~Empty();
Empty& operator=(const Empty&);
Empty& operator=(const Empty&&); // after C++ 11
Empty* operator &();
const Empty* operator &() const;
}
继承概念
目的:基于目标代码的复用
思想:对事物进行分类。派生类是基类的具体化。把事物(概念)以层次结构表示出来,有利于描述和解决问题。
可用于:增量开发
多态
同一论域中一个元素可有多种解释
- 提高语言灵活性
- 程序设计语言
- 一名多用——函数重载
- 类属——模板
- OO 程序设计——虚函数
运算符重载
- 动机:使用操作符的语义。为自定义数据类型提供类似内置类型的操作方式。
- 作用:提高可读性、可扩充性
article link="/posts/C++拾遗/运算符重载/" >}}
对象切片
将派生类对象赋值给基类对象时,会发生对象切片。派生类对象会变成基类,只有其基类部分的成员被保留。
class Base{
public:
virtual void foo() { cout<<"Base"; }
};
class Derived: public Base{
public:
void foo() override { cout<<"Derived"; }
}
void function(Base base){
base.foo();
}
Derived derived;
function(derived); // Base
从结果上来说,把 derived 的数据赋给一个 base 对象的内存是不现实的,因为一般来说 sizeof Derived 比 sizeof Base 大。
避免发生切片的方法是使用引用或者指针。
void function(Base& base){ // use Reference
base.foo();
}
Derived derived;
function(derived); // Derived
其他
异常处理
article link="/posts/C++拾遗/异常处理/" >}}
右值引用
article link="/posts/C++拾遗/移动语义与右值引用/" >}}