仅供个人复习,
C语言IO占位符表
%d | 十进制整数(int) |
---|---|
%ld | long |
%lld | long long |
%u | unsigned int |
%o | 八进制整型 |
%x | 十六进制整数/字符串地址 |
%c | 单个字符 |
%s | 字符串 |
%f | float,默认保留6位 |
%lf | double |
%e | 科学计数法 |
%g | 根据大小自动选取f或e格式,去掉无效0 |
转义符表
转义符可以取消关键字符的特殊性,下面是常见的转义符使搭配
printf("\b");//退格符
printf("\n");//换行
printf("\a");//响铃,电脑真的响一下,不可思议
printf("\t");//水平制表符
printf("\v");//垂直制表符
printf("\130");//输出char对应130的字符
printf("%% %d",12);//%的转义使用%,而不是\
随机数生成
srand((unsigned int)time(NULL));
int ret1 = rand() % 10 + 1;//生成1~10的随机数
int ret2 = rand() % 100 + 1;//生成1~100的随机数
int ret3 = rand() % 34 + 66;//生成66~99的随机数
int ret4 = rand() % (n - m + 1) + m;//生成m~n的随机数
//规律是第一个数+第二个-1,是上界
//第二个数是下界
C/C++ 语言数据类型大小
ANSI/ISO规范
sizeof(short int)<=sizeof(int)
sizeof(int)<=sizeof(long int) short
int至少应为16位(2字节)
long int至少应为32位。
16位编译器
数据类型 | 字节数 |
---|---|
char | 1 |
short | 2 |
int | 2 |
long | 4 |
float | 4 |
double | 8 |
bool | 1 |
指针 | 2 |
32位编译器
数据类型 | 字节数 |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
long long | 8 |
float | 4 |
double | 8 |
bool | 1 |
long double | 12 |
指针 | 4 |
64位编译器
数据类型 | 字节数 |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 8 |
long long | 8 |
float | 4 |
double | 8 |
bool | 1 |
long double | 16 |
指针 | 8 |
宏函数和内联函数的区别
宏函数(Macro Functions):
-
替换规则:在编译预处理阶段的简单文本替换
-
参数展开:没有参数类型检查
-
适用场景:简单的、短小的代码片段,例如进行简单的数学计算、位操作等。
内联函数(Inline Functions):
-
替换规则:编译阶段处理。编译器会尝试将函数调用处直接替换为函数体。
-
参数类型检查:参数和返回值检查与正常函数无异
-
适用场景:内联函数适用于较短小的函数,包含一些简单的代码逻辑,且频繁调用的情况。
-
推荐用法:在类里定义的函数都是默认加上inline关键字的(是不是由编译器决定),或者在头文件中避免函数重定义问题。inline关键字只能建议编译器进行内联,是否采纳取决于编译器
内联函数可以优化函数进入和离开的开销,但内联可能会导致编译后的代码体积增大,宏函数的使用更需要小心,因为它在文本替换阶段可能会引发一些意想不到的问题。
差异案例
#include <iostream>
#define SQUARE_MACRO(x) x * x
inline int square_inline(int x) {
return x * x;
}
int main() {
int num = 5;
// result = (++num) * (++num) 直接把参数替换进函数
int result_macro = SQUARE_MACRO(++num); // 使用宏函数
num = 5;
//先自增再传入
int result_inline = square_inline(++num); // 使用内联函数
std::cout << "Number: " << num << std::endl;
std::cout << "Result (Macro): " << result_macro << std::endl;
std::cout << "Result (Inline): " << result_inline << std::endl;
return 0;
}
STL标准库
通用的算法API
条件排序
#include <algorithm>
// 使用 lambda 表达式按照姓名升序排序
std::sort(v.begin(), v.end(),
[](const Person& a, const Person& b)
{
return a.name < b.name;
});
//运算符重载
struct Person {
int age;
Person(int a) : age(a) {
}
// 重载 < 运算符,按照年龄升序排序
bool operator<(const Person& other) const {
return age < other.age;
}
};
sort(v.begin(),v.end());
移除符合条件的元素
#include <algorithm>
//返回的是移除后的逻辑结尾的迭代器,将符合条件的置于尾端
auto newEnd = std::remove_if(v.begin(), v.end(), [](T item)
{
return ...
});
//真正的移除
numbers.erase(newEnd, numbers.end()); // 删除废弃的元素
vector
优点:
- 内存是连续分配的,访问元素的速度较快。
- 在末尾插入和删除元素的时间复杂度为常数。
缺点:
- 在中间插入或删除元素的时间复杂度较高,需要移动后续元素。
- 在内存不足时,可能会导致重新分配内存和复制元素。
#include <vector>
std::vector<int> v;
v.push_back(10); // 在末尾插入元素
v.pop_back(); // 删除末尾的元素
v.size(); // 返回容器中的元素数量
v.empty(); // 检查容器是否为空
v.clear(); // 清空容器中的所有元素
v.at(index); // 访问指定索引的元素
v.begin(); // 返回指向容器第一个元素的迭代器
v.end(); // 返回指向容器最后一个元素之后的迭代器
list
优点:
- 支持在任意位置快速插入和删除元素。
- 在中间插入和删除元素的时间复杂度为常数。
缺点:
- 元素在内存中不连续存储,访问元素的速度慢。
- 占用更多内存,每个节点需要存储额外的指针。
#include <list>
std::list<int> lst;
lst.push_back(10); // 在末尾插入元素
lst.push_front(20); // 在开头插入元素
lst.pop_back(); // 删除末尾的元素
lst.pop_front(); // 删除开头的元素
lst.size(); // 返回容器中的元素数量
lst.empty(); // 检查容器是否为空
lst.clear(); // 清空容器中的所有元素
lst.front(); // 访问首元素
lst.back(); // 访问末尾元素
lst.begin(); // 返回指向容器第一个元素的迭代器
lst.end(); // 返回指向容器最后一个元素之后的迭代器
forawrd_list
与list类似,仅支持单向访问,效率更佳一些
#include <forward_list>
std::forward_list<T> fl;
fl.push_front(const value_type& value); // 在头部插入元素
fl.pop_front(); // 从头部删除元素
fl.insert_after(pos, const value_type& value); // 在指定位置后插入元素
fl.erase_after(pos); // 在指定位置后删除元素
fl.front(); // 访问第一个元素
fl.begin(); // 返回指向第一个元素的迭代器
fl.end(); // 返回指向最后一个元素之后的迭代器
deque
优点:
- 支持在两端快速插入和删除元素。
- 内存是分块分配的,访问元素的速度较快。
缺点:
- 难以在中间插入或删除元素
- 存储多个分块,占用较多内存
#include <deque>
std::deque<int> dq;
dq.push_back(10); // 在末尾插入元素
dq.push_front(20); // 在开头插入元素
dq.pop_back(); // 删除末尾的元素
dq.pop_front(); // 删除开头的元素
dq.size(); // 返回容器中的元素数量
dq.empty(); // 检查容器是否为空
dq.clear(); // 清空容器中的所有元素
dq.front(); // 访问首元素
dq.back(); // 访问末尾元素
dq.begin(); // 返回指向容器第一个元素的迭代器
dq.end(); // 返回指向容器最后一个元素之后的迭代器
map
优点:
- 存储键值对,支持按键进行高效的查找和插入。
- 根据键的顺序遍历元素。
缺点:
- 内存使用较多,每个键值对都需要额外的内存存储键。
- 没有连续存储,访问元素的速度相对较慢。
#include <map>
std::map<std::string, int> m;
m["one"] = 1; // 插入键值对
m["two"] = 2;
m.find(const key_type& k); // 查找键的位置
m.count(const key_type& k); // 计算具有特定键的元素数量
m.size(); // 返回容器中的键值对数量
m.empty(); // 检查容器是否为空
m.clear(); // 清空容器中的所有键值对
m.begin(); // 返回指向容器第一个键值对的迭代器
m.end(); // 返回指向容器最后一个键值对之后的迭代器
set
优点:
- 存储唯一的元素,支持按值进行高效的查找和插入。
缺点:
- 内存使用较多,每个元素都需要额外的内存存储。
- 不连续存储,访问元素的速度相对较慢。
#include <set>
std::set<int> s;
s.insert(const value_type& val); // 插入元素
s.find(const key_type& k); // 查找元素
s.size(); // 返回容器中的元素数量
s.empty(); // 检查容器是否为空
s.clear(); // 清空容器中的所有元素
s.begin(); // 返回指向容器第一个元素的迭代器
s.end(); // 返回指向容器最后一个元素之后的迭代器
unordered_map (C++11)
优点:
- 使用哈希表实现,支持快速的查找和插入操作,平均时间复杂度为常数。
- 对于大数据集,查找效率高于std::map。
缺点:
- 内存占用较高,需要存储哈希表和键值对。
- 不保证元素的顺序。
#include <unordered_map>
std::unordered_map<std::string, int> um;
um["one"] = 1; // 插入键值对
um["two"] = 2;
um.find(const key_type& k); // 查找键的位置
um.count(const key_type& k); // 计算具有特定键的元素数量
um.size(); // 返回容器中的键值对数量
um.empty(); // 检查容器是否为空
um.clear(); // 清空容器中的所有键值对
um.begin(); // 返回指向容器第一个键值对的迭代器
um.end(); // 返回指向容器最后一个键值对之后的迭代器
unordered_set (C++11)
优点:
- 使用哈希表进行实现,支持快速的查找和插入操作,平均时间复杂度为常数。
- 对于大数据集,查找效率高于std::set。
缺点:
- 内存占用较高,因为需要存储哈希表和元素。
- 不保证元素的顺序。
#include <unordered_set>
std::unordered_set<int> us;
us.insert(const value_type& val); // 插入元素
us.find(const key_type& k); // 查找元素
us.size(); // 返回容器中的元素数量
us.empty(); // 检查容器是否为空
us.clear(); // 清空容器中的所有元素
us.begin(); // 返回指向容器第一个元素的迭代器
us.end(); // 返回指向容器最后一个元素之后的迭代器
stack
#include <stack>
std::stack<T> s;
s.push(const value_type& value); // 将元素压入堆栈顶部
s.pop(); // 弹出堆栈顶部的元素
s.top(); // 访问堆栈顶部的元素
s.empty(); // 检查堆栈是否为空
s.size(); // 返回堆栈中元素的数量
queue
#include <queue>
std::queue<T> q;
q.push(const value_type& value); // 将元素推入队列尾部
q.pop(); // 从队列头部弹出元素
q.front(); // 访问队列头部元素
q.back(); // 访问队列尾部元素
q.empty(); // 检查队列是否为空
q.size(); // 返回队列中元素的数量
priority_queue
#include <queue>
std::priority_queue<T> pq;
pq.push(const value_type& value); // 将元素推入优先队列
pq.pop(); // 从优先队列中弹出元素
pq.top(); // 访问优先队列中优先级最高的元素
pq.empty(); // 检查优先队列是否为空
pq.size(); // 返回优先队列中元素的数量
#include <iostream>
#include <queue>
#include <vector>
struct MyStruct {
int value;
// 比较操作符,根据 value 来比较 ,越大优先级越高
bool operator<(const MyStruct& other) const {
return value < other.value;
}
};
int main() {
std::priority_queue<MyStruct> pq;
pq.push({
5});
pq.push({
2});
pq.push({
8});
pq.push({
1});
// 遍历优先队列按优先级输出
while (!pq.empty()) {
std::cout << pq.top().value << " ";
pq.pop();
}
return 0;
}
// 8 5 2 1
智能指针
- std::shared_ptr:允许多个智能指针共享同一个对象,通过引用计数来管理对象的生命周期。当最后一个引用被释放时,对象会被销毁。
auto sp = std::make_shared<int>(); // 分配堆空间,创建智能指针
auto sp2 = sp; // 创建另一个智能指针
-
std::unique_ptr:用于独占地拥有一个对象,不能被多个智能指针共享。它提供了更轻量级的智能指针,适用于不需要共享所有权的情况。
-
std::weak_ptr:用于解决std::shared_ptr的循环引用问题。它可以与std::shared_ptr一起使用,但不会增加对象的引用计数。
构造函数执行顺序
- 先成员的构造,再当前类型的构造
- 父类构造优先于子类构造
- 成员初始化按书写顺序,低于构造顺序
- 虚基类只构造一次,非虚构造两次
例题:问输出结果是多少
#include <iostream>
class A {
public:
A() {
std::cout << "A Constructor" << std::endl;
}
};
class B : public A {
public:
B() {
std::cout << "B Constructor" << std::endl;
}
};
class C : public A {
public:
C() {
std::cout << "C Constructor" << std::endl;
}
};
class D : public B, public C {
public:
D() {
std::cout << "D Constructor" << std::endl;
}
};
int main() {
D d;
return 0;
}
析构函数: 子类>子类成员>父类>父类成员
一个冷知识:一个类的构造函数和析构函数都是public才能被外界实例化
如果是protected只能在派生类实例化和析构
也就是说得在当前位置能对该类析构和构造才能被实例化(掂量一下访问权限在这里是否是同时有)
继承的理解
C++中
如果A派生于B,那么在创建A的实例时,会形成如下的结构,也就是说A持有一个B的实例
class B
{
继承方式(public,protected,private) A parent;
};
可能这样看起来很奇怪,但是实际上却是编译器为我们建立了一个特殊的关系,让我们能直接让B能拥有A的一切,而不是B.A.XXX,相当于一种缩略的调用方式。基于此我们能产生更加深入的理解,B间接持有了A的内存。
这样,我们创建一个B的实例,可以得知,构造和析构,初始化顺序,访问权限等一系列规则。
基类成员 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public 成员 | public 成员 | protected 成员 | 不可访问 |
protected 成员 | protected 成员 | protected 成员 | 不可访问 |
private 成员 | 不可访问 | 不可访问 | 不可访问 |
实际上就是等同于通过B的实例,我们能否访问A中的成员。
B通过private继承,显然无法访问成员变量A,更不谈A内的访问权限了
B通过protected继承,显然是只有继承了B才能访问A成员,接着才能谈A内的权限问题
B通过public继承,能够完全访问A,此时访问权限就和A定义的一样,
只需要记住:何种继承方式决定能否访问基类,基类的何种修饰符决定能否访问基类的成员
public为公开,不设限
protected为继承关系可访问
private为当前类(友元也包含在内)可以访问
构造顺序则与上面提到的无异。
(virtual) 虚基类和虚函数原理
普通函数的继承覆盖
class B {
public:
void Test()
{
cout << "B"<<endl;
}
};
class C : public B {
public:
void Test()
{
cout << "C"<<endl;
}
};
int main() {
C* c = new C();
c->Test();
((B*)c)->Test();
return 0;
}
这样的执行结果是
证明普通函数在执行时与当前的数据类型相关(如指针类型),当前指针类型为什么,则执行哪个类型的函数。
虚函数
在函数前增加virtual关键字即可定义虚函数,虚函数在整个作用过程发生以下行为
- 编译器生成虚函数表(为每个函数定义了指向真正函数的指针称为虚表指针)
- 不论当前的指针是何种类型,通过查询虚函数表找到真正的函数
原理非常简单,就是为同名函数建立了一个表格,一个函数指向了一个真正的实现,根据继承关系不断产生覆盖。像查字典一样
#include <iostream>
using namespace std;
class A {
private:
virtual void Test()
{
cout << "A";
}
};
class B : public A {
public:
void Test()
{
cout << "B";
}
};
class C : public B {
public:
void Test()
{
cout << "C";
}
};
int main() {
C* c = new C();
((B*)c)->Test();
return 0;
}
(可以去掉virtual得到CB输出)
且虚函数有如下特性
- 与访问权限关键字无关(无论何种权限都会建立虚函数表),与继承关系有关(在继承树中具备高深度优先,和唯一性)
- 查询虚函数表降低了函数效率
虚基类
虚基类旨在解决菱形继承问题,如果按照下面这张图的继承关系
如果我们不采用虚基类,则按照上面我们提到的继承的内存原理,实例应该是这样的
这样不仅导致了内存浪费,而且导致从B和C修改的数据不同步。
class A {
public:
int x;
};
class B : virtual public Base {
public:
int y;
};
class C : virtual public Base {
public:
int z;
};
class D : public B, public C {
public:
int w;
};
在这里千万注意构造函数的问题,如果是虚,则创建D的构造顺序是A->B->C->D
如果不是,则A->B->A->C->D
转换函数
explict构造
显式构造函数
class MyClass {
public:
explicit MyClass(int x) {
this->x = x;
}
void print() {
std::cout << x << std::endl;
}
private:
int x;
};
int main() {
MyClass obj1 = 42; // 错误,不能隐式转换
MyClass obj2(42); // 正确,需要显式调用构造函数
obj2.print(); // 输出 42
return 0;
}
特别的,对于 non-explict-one-argument ctor
- 非explict
- 单参数(默认参数也计数)
这时我们可以
也要小心这种情况:
编译器把Fraction转为double与4进行运算,但是结果右值double没有向Fraction的转换
相反的我们把4放到前面则编译通过
模板特化
全特化
template <typename T>
class MyTemplate {
public:
void foo() {
// 通用实现
}
};
template <>
class MyTemplate<int> {
public:
void foo() {
// 专门为int类型的特化实现
}
};
偏特化
特定的偏
范围的偏
今天到这里,改日再更