编码的能力提升方法:阅读 码代码 无限循环
声明:本文截图以及相关内容多来自于一个微信公众号“Lion 莱恩呀” ,博主水平很高,本文属于学习笔记。
2024.10.12
线程锁
2024.09.10
1.设计模式之观察者模式
不采用设计模式:
class DisplayA {
public:
void Show(float temperature);
};
class DisplayB {
public:
void Show(float temperature);
};
class DisplayC {
public:
void Show(float temperature);
}
class WeatherData {
public:
float getTemperature() const { return _temperature; }
void setTemperature(float t) { _temperature = t; }
private:
float _temperature = 0.0f;
};
class DataCenter {
public:
void TempNotify() {
DisplayA *da = new DisplayA;
DisplayB *db = new DisplayB;
DisplayC *dc = new DisplayC;
// DisplayD *dd = new DisplayD;
float temper = this->CalcTemperature();
da->Show(temper);
db->Show(temper);
dc->Show(temper);
}
private:
float CalcTemperature() {
WeatherData *data = new WeatherData();
data->setTemperature(22.1f);
float temper = data->getTemperature();
delete data;
return temper;
}
};
int main() {
DataCenter *center = new DataCenter;
center->TempNotify();
return 0;
}
从以上代码可以看出:代码不符合 接口隔离、 面向接口编程(DataCenter数据中心类 与 DisplayA、B、C 展示类 之间关联性太强)的设计原则。
优化后:
/*
* 2024.09.10 观察者模式
*
* */
class IDisplay{
public:
virtual void Show(float temperature) = 0;
virtual ~IDisplay(){}
};
class DisplayA: public IDisplay{
public:
void Show(float temperature) override{
cout<<"DisplayA Show" <<endl;
}
private:
void jianyi();
};
class DisplayB : public IDisplay{
public:
void Show(float temperature)override{
cout<<"DisplayB Show"<<endl;
}
};
class DisplayC : public IDisplay{
public:
void Show(float temperature) override{
cout<<"DisplayC Show"<<endl;
}
};
class WeatherData{
public:
float getTemperature() const {return _temperature;}
void setTemperature(float t){_temperature =t;}
private:
float _temperature = 0.0f;
};
//应对稳定点 :抽象
//应对变化点:扩展(继承和组合)
class DataCenter{
public:
// void TempNotify(){
// DisplayA* da = new DisplayA;
// DisplayB* db = new DisplayB;
// DisplayC* dc = new DisplayC;
// float temper = this->CalcTemperature();
// da->Show(temper);
// db->Show(temper);
// dc->Show(temper);
// }
void Attach(IDisplay* ob){
//添加设备
obs.push_back(ob);
}
void Detach(IDisplay* ob){
//移除设备
obs.remove(ob);
}
void Notify(){
WeatherData* data = new WeatherData();
data->setTemperature(CalcTemperature());
float temper = data->getTemperature();
for(auto iter : obs){
iter->Show(temper);
}
delete data;
}
//接口隔离
private:
float CalcTemperature(){
float temper = 22.1f;
return temper;
}
std::list<IDisplay*> obs;
};
int main() {
/*
* 2024.09.10 观察者模式
* */
DataCenter* center = new DataCenter;
IDisplay* da = new DisplayA();
center->Attach(da);
IDisplay* db = new DisplayB();
center->Attach(db);
IDisplay* dc = new DisplayC();
center->Attach(dc);
center->Notify();
center->Detach(db);
center->Notify();
delete center;
delete da;
delete db;
delete dc;
return 0 ;
}
接口隔离:类与类之间的依赖应该通过具体的接口来实现,而不是依赖于具体的类实现
DataCenter类与多个显示设备(DisplayA、DisplayB、DisplayC)之间的依赖是通过IDisplay接口来实现的。这种设计符合接口隔离原则
面向接口编程:它强调程序的设计应依赖于抽象而非具体实现。这意味着你的代码应该与接口交互,而不是与实现这些接口的具体类交互。
还体现了一下设计原则:
单一职责原则 (Single Responsibility Principle, SRP)
虽然这不是你直接询问的原则,但它在你的代码中也有所体现。单一职责原则指出一个类应该只有一个改变的原因。在你的代码中,DataCenter类负责管理和通知观察者,而WeatherData类负责处理温度数据。这种分离使得每个类都有一个明确的责任,这有助于代码的维护和扩展。
开闭原则 (Open-Closed Principle, OCP)
开闭原则指出软件实体(类、模块、函数等)应该是可扩展的,但不可修改的。也就是说,你可以通过扩展软件实体的行为来满足新的需求,而无需修改现有代码。在你的观察者模式实现中,如果将来需要增加新的显示设备,你只需要创建一个新的类来继承IDisplay,并实现实体方法,而不需要更改DataCenter的代码。这样就遵循了开闭原则。
依赖倒置原则 (Dependency Inversion Principle, DIP)
依赖倒置原则指出高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。在你的代码中,DataCenter类依赖于IDisplay接口,而不是具体的DisplayA、DisplayB或DisplayC类。这样,DataCenter就可以与任何实现了IDisplay接口的类一起工作,而不必关心具体的实现细节。
2. 策略模式
未使用设计模式:
使用模式后:
/*
* 策略模式
*/
class Context{
};
class ProStategy{
public:
virtual double CalcPro(const Context &ctx) = 0;
virtual ~ProStategy();
};
class VAC_Spring : public ProStategy{
public:
double CalcPro(const Context &ctx) override{
cout<<" Spring Vacation"<<endl;
}
};
class VAC_Qixi : public ProStategy{
public:
double CalcPro(const Context &ctx) override{
cout<<"QIXI vacation"<<endl;
}
};
class Promotion{
public:
Promotion(ProStategy* stategy) : _s(stategy){}
~Promotion(){}
double CalcPromotion(const Context& ctx){
return _s->CalcPro(ctx);
}
private:
ProStategy* _s;
};
int main() {
/*
* 策略模式:
* 稳定点: 促销活动
* 变化点: 七夕 国庆 等算法
* */
Context ctx;
ProStategy*s = new VAC_Qixi();
Promotion* p = new Promotion(s);//依赖注入:两个类的依赖性的只建立在一个接口上面
p->CalcPromotion(ctx);
return 0;
}
2024.09.09
1.再聊 private protected
两个区别:
相同点:类对象无法直接使用private 和protected成员
不同点:private成员无法被子类使用
protected 成员可以被子类使用
在设计类时,需要根据成员的访问需求来选择合适的访问控制关键字。如果希望某个成员只在类内部可见,则应将其声明为 private
;如果希望该成员在类内部和派生类中都可见,但对外部隐藏,则应将其声明为 protected
。
2. 设计模式:模版方法
特点是:骨架流程 子类protected 实现 可以实现子类方便扩展
/*
* 2024.09.09 设计模式~模版方法
*
* */
//开闭原则
class ZooShow{
public:
void Show(){
if(Show0())
PlayGame();
Show1();
Show2();
Show3();
}
private:
void PlayGame(){
cout << "after Show0,then play game"<<endl;
}
//对用户隐藏,但是对子类开放
protected:
bool expired;
virtual bool Show0(){
cout<<"Show0"<<endl;
if(!expired){
return true;
}
return false;
}
virtual void Show1(){
}
virtual void Show2(){
cout<<"show2"<<endl;
}
virtual void Show3(){
}
};
//模版方法模式:有算法骨架,在子类中延迟实现protected
class ZooShowEx1 : public ZooShow{
protected:
bool Show0() override {
cout<<"ZooShowEx1 show0"<<endl;
if(!expired){ // 里氏替换原则
return true;
}
return false;
}
};
class ZooShowEx2:public ZooShow{
protected:
void Show1() override{
cout<<"show1"<<endl;
}
void Show2() override{
cout<<"show3"<<endl;
}
};
class ZooShowEx3 : public ZooShow{
protected:
void Show1() override{
cout<<"show1"<<endl;
}
void Show3() override{
cout<<"show3"<<endl;
}
};
int main() {
/*2024.09.09
*设计模式之模版方法
* */
ZooShow* zs = new ZooShowEx1;
zs ->Show();
return 0
}
2024.09.06
1.设计模式之单例 以及 工厂模式
不使用设计模式的话,代码实现如下:
以上提到的总结起来就是 需要两个接口:创建对象的接口 和 具体职责功能接口,要分离开来,因为客户只关心功能职责。 因此就有以下代码:
有点复杂,慢慢在使用中理解
2. 动态库 静态库区别
2024.09.05
1. 设计模式
多态的核心原理:
九大设计原则:
里氏替换:子类不可破坏父类非抽象方法
多用组合,少用继承
2024.09.04
四种强制类型转换方法:
来两个概念:
dynamic_cast不支持基本类型转换。
const_cast 还不太明白
2024.09.02
stack栈的学习
栈的构造函数:
重点:
2024.08.29
双端队列deque
#include <deque>
#include <algorithm>
int main() {
/*
* 2024.08.29 双端队列
* */
deque<char> players {'A','B','C','D','E'};
deque<int> scores;
for(int i=0; i<10; i++){
int score = rand()%10 + 1;
scores.push_back(score);
}
sort(scores.begin(),scores.end());
scores.pop_back();
scores.pop_front();
double average = 0.0;
for(const auto& score: scores){
average += score;
}
average /= scores.size();
cout<<"选手:"<<players.front()<<endl;
cout<<"平均分:"<< average<<endl;
return 0;
}
对比:双端队列有迭代器,支持随机存取 首尾删除 与 vector对比:
在C++中,
deque
(双端队列)和vector
(向量)都是STL(标准模板库)中提供的序列容器,用于存储元素集合。尽管它们在某些方面相似,但在内部实现、性能特征和使用场景上存在显著差异。以下是它们之间的详细对比:1. 内部实现
deque
:
- 使用了一种分段连续存储的结构,由多个固定大小的块(chunks)组成,每个块内部存储一部分元素。
- 这些块通过指针或数组索引连接起来,形成一个逻辑上的连续序列。
- 允许在两端高效地添加或删除元素,因为只需调整相关块的指针或索引即可。
vector
:
- 使用一块连续的内存来存储元素,类似于动态数组。
- 当需要添加更多元素而当前容量不足时,会重新分配一块更大的内存区域,并将现有元素复制到新区域中(这可能会导致性能开销)。
- 支持通过索引直接访问任意位置的元素,因为元素在内存中是连续存储的。
2. 性能特征
- 随机访问性能:
deque
虽然也支持随机访问,但由于元素分布在多个块中,访问不同位置的元素可能需要更多的指针操作,因此相对于vector
,其随机访问的性能稍差一些。vector
的元素在内存中是连续存储的,因此它支持高效的随机访问,可以通过索引直接访问任意位置的元素。- 插入和删除操作性能:
deque
在两端进行插入和删除操作的性能较好,因为它可以在常数时间内在两端进行操作,而不需要移动其他元素。vector
在尾部进行插入和删除操作的性能也很好(时间复杂度为O(1),但在容量不足重新分配内存时除外),但在中间或头部进行插入和删除操作时,需要移动后续元素,因此性能较差(时间复杂度为O(n))。- 内存管理:
deque
相对于vector
具有更高的内存开销,因为它需要为每个块分配额外的内存空间来存储指针或索引,以及保持块之间的连接。vector
则在内存中分配一块连续的内存来存储元素,可以更有效地利用内存,但可能需要更频繁的内存重新分配和元素复制。3. 使用场景
- 如果需要频繁在两端进行插入和删除操作,或者不需要频繁的随机访问,
deque
可能是一个更好的选择。- 如果需要高效的随机访问和在尾部进行插入和删除操作,
vector
可能更适合。4. 迭代器和引用稳定性
- 在
vector
中,插入或删除元素可能会使指向元素的迭代器失效,特别是当在迭代器指向的元素之前或之后插入或删除元素时。- 在
deque
中,迭代器失效的情况较少,但如果在插入或删除操作导致重新分配块时,指向被删除元素的迭代器将失效。然而,在deque
的当前块内进行插入或删除操作时,迭代器通常仍然有效。综上所述,选择
deque
还是vector
取决于具体的使用场景和需求。在实际开发中,应根据程序的性能要求、内存使用情况和操作特性来做出合理的选择。
2024.08.28
队列queue 生产者消费者
queue为什么没有迭代器?
std::queue 是 C++ 标准库中的一个容器适配器,它提供了一种先进先出 (FIFO) 的数据结构。与 std::vector、std::list 或 std::deque 这些容器不同,std::queue 没有提供迭代器接口。原因如下:
设计目的:
std::queue 主要是为了提供队列的基本操作(如入队 push 和出队 pop)而设计的。它的主要目的是管理队列的顺序访问,而不是随机访问。
内部实现:
std::queue 内部通常使用 std::deque 或 std::list 来存储数据。尽管这些容器提供了迭代器,但 std::queue 的设计意图并不是为了让用户直接访问底层容器的数据。
访问限制:
std::queue 只提供了对队首元素的访问(通过 front()),并且允许在队尾添加元素(通过 push())和从队首移除元素(通过 pop())。这样的设计使得直接提供迭代器变得没有必要。
安全性和简单性:
通过不提供迭代器,std::queue 保持了其功能的简洁性和安全性。避免了由于直接访问底层容器而导致的潜在问题。
如果您需要访问 std::queue 中的所有元素,可以通过以下几种方式实现:遍历:
您可以通过不断地调用 front() 和 pop() 来访问队列中的所有元素。注意,这样做会清空队列。
辅助容器:
您可以将队列中的元素复制到另一个容器(如 std::vector 或 std::deque),然后使用该容器的迭代器进行访问。
//生产者消费者 queue队列
#include <mutex>
#include <condition_variable>
#include <thread>
const int MAX_QUEUE_SIZE =5;
queue<int> dataQueue;
mutex mtx;
condition_variable condtion;
void producer(int id){
for(int i = 1;i<= 10;++i){
unique_lock<mutex> lock(mtx);
condtion.wait(lock,[]{return dataQueue.size() < MAX_QUEUE_SIZE;});
//队列大小小于额定大小条件为真true时 解锁返回true,向下运行
dataQueue.push(i);
cout<<"Producer "<<id<<"produced: "<< i <<endl;
lock.unlock();
condtion.notify_all();//通知消费者
}
}
void consumer(int id){
for(int i =1;i<=10;++i){
unique_lock<mutex>lock(mtx);
condtion.wait(lock,[]{return !dataQueue.empty();});//等待缓存区不空
int data = dataQueue.front();
dataQueue.pop();
cout<<" Consumer"<<id<<" consumed: "<< data<<endl;
lock.unlock();
condtion.notify_all();//通知生产者可以生产数据
}
}
int main() {
/*
* 2024.08.28
* 生产者与消费者 queue 队列
* */
vector<thread>producers,consumers;
for(int i=0;i<3;i++){
producers.emplace_back(producer, i);
}
for(int i=0;i<2;i++){
consumers.emplace_back(consumer,i);//直接传对象
}
//等待所有生产者完成
for(auto& p:producers){
p.join();
}
unique_lock<mutex> lock(mtx);
condtion.notify_all();
//等待消费者完成
for(auto& c:consumers){
c.join();
}
// thread producerThread(producer);
// thread consumerThread(consumer);
// producerThread.join();
// consumerThread.join();
return 0;
}
2024.08.26
回去参加妹妹婚礼,停更5天,继续加油 年轻人
1.Vector的使用
迭代器:Myfirst Mylast _Myend
注意:
1. 使用at 超出范围会报异常;[ ] 则不会出现异常提示
2.
3. vector扩容:
//vector扩容 std::vector<int> arr; std::size_t prev_capacity = arr.capacity(); std::size_t count = 0; for (int i = 0; i < 1000; ++i) { arr.push_back(i); if (arr.capacity() != prev_capacity) { ++count; std::cout << "第" << count << "次分配空间:" << arr.capacity() << std::endl; prev_capacity = arr.capacity(); } }
结果:
2024.08.14
1. 容器与适配器,分为QT 和 c++ STL来总结:
(1)c++ 中队列和链表的区别:
在 C++ 中,队列和链表是两种不同的数据结构,它们有不同的用途和特性。下面详细解释两者的区别:
1. 数据结构类型
链表:
- 链表是一种线性数据结构,其中元素是以节点的形式存储的,每个节点包含数据和指向下一个节点的指针。链表可以是单向的(只有一个指向下一个节点的指针)或双向的(同时包含指向下一个节点和前一个节点的指针)。
- 链表不保证元素的连续存储,这使得插入和删除操作非常高效,但随机访问效率较低。
队列:
- 队列是一种抽象数据类型,它遵循先进先出(FIFO,First In First Out)的原则。队列通常用于处理按照一定顺序到达的任务或数据项。
- 队列可以使用不同的底层数据结构来实现,如数组、链表或双端队列(
std::deque
)等。2. C++ 标准库实现
链表:
std::list
是 C++ 标准库中提供的双向链表容器。std::forward_list
是 C++ 标准库中提供的单向链表容器。队列:
std::queue
是一个队列适配器,它基于其他容器(如std::vector
、std::deque
或std::list
)实现。std::priority_queue
是一个优先队列容器适配器,它通常基于std::vector
或std::deque
实现。3. 主要操作
链表:
- 主要操作包括:插入、删除和遍历。
- 插入和删除操作通常非常高效(O(1)),因为只需要更改相邻节点的指针即可。
- 遍历和随机访问通常较慢(O(n)),因为需要从头节点开始顺序访问。
队列:
- 主要操作包括:入队(enqueue)、出队(dequeue)和获取队首元素。
std::queue
和std::priority_queue
提供了push
、pop
和front
等操作。std::deque
可以在两端进行高效的插入和删除操作,但作为队列使用时通常只在一端进行操作。4. 使用场景
链表:
- 适用于需要频繁插入和删除元素的场景。
- 适用于不需要随机访问元素的场景。
队列:
- 适用于需要按照先进先出原则处理数据的场景。
- 适用于任务调度、消息传递等场景。
5. 内存管理
链表:
- 需要额外的内存来存储指针。
- 动态分配内存,每个节点的大小可能不同。
队列:
- 使用底层容器的内存管理策略。
- 可能使用连续内存块(如数组或
std::vector
)。
(2)适配器与容器的区别
在 C++ 中,适配器和容器是两种不同的概念,它们在标准库中扮演着不同的角色。下面详细解释这两者之间的区别:
容器(Container)
容器是 C++ 标准库中用于存储和组织数据的基本结构。容器提供了对元素的访问、插入、删除和其他操作。容器通常负责管理存储空间,并为数据提供一定的访问和修改接口。
容器的特点:
- 数据管理:容器负责数据的存储和管理。
- 接口:容器提供了一系列接口来访问和修改数据,例如
push_back
、insert
、erase
等。- 性能:容器设计时考虑到了性能因素,例如
std::vector
保证了随机访问的效率。- 类型:常见的容器包括
std::vector
、std::list
、std::deque
、std::set
、std::map
等。适配器(Adapter)
适配器是对现有容器进行封装或扩展的一种工具,它改变了容器的接口或行为,以便能够满足特定的需求。适配器不拥有数据,而是建立在现有容器的基础上,提供一种新的使用方式。
适配器的特点:
- 封装:适配器封装了容器的行为,以提供不同的接口。
- 扩展:适配器可以扩展容器的功能,例如通过改变容器的行为来实现队列或栈。
- 重用:适配器允许重用已有的容器,从而避免重复实现相似功能。
- 类型:常见的适配器包括
std::stack
、std::queue
、std::priority_queue
等。
(3) QT中的容器和适配器有哪些?
在 Qt 库中,容器类和适配器是用来管理和操作数据的重要组件。Qt 提供了一套丰富的容器类和适配器,这些类继承自 Qt 的基础容器框架,提供了类似于 C++ 标准库中的容器和适配器的功能。下面详细介绍 Qt 中的容器类和适配器。
容器类
Qt 提供了多种容器类,它们分别对应 C++ 标准库中的容器,但通常提供了更丰富的功能和更好的跨平台支持。以下是 Qt 中的一些容器类:
QVector
:
- 类似于
std::vector
,是一个动态数组,支持快速的随机访问和高效的元素插入/删除。- 使用连续的内存块存储数据。
QList
:
- 类似于
std::list
,是一个双向链表,支持高效的元素插入/删除。- 不支持随机访问。
QLinkedList
:
- 也是一个双向链表,与
QList
类似,但提供了不同的性能特点。- 通常用于需要频繁插入和删除元素的情况。
QStack
:
- 是一个栈容器,支持 LIFO(Last In First Out)的行为。
- 基于
QList
实现。
QQueue
:
- 是一个队列容器,支持 FIFO(First In First Out)的行为。
- 基于
QList
实现。
QSet
:
- 类似于
std::set
,是一个不重复元素的集合。- 使用哈希表实现,提供了快速的查找和插入。
QHash
:
- 类似于
std::unordered_map
,是一个键值对的容器。- 使用哈希表实现,提供了快速的查找和插入。
QMap
:
- 类似于
std::map
,是一个键值对的容器,按键排序。- 使用红黑树实现,提供了有序的查找和插入。
QMultiMap
和QMultiHash
:
- 分别是
QMap
和QHash
的多值版本,允许相同的键对应多个值。适配器
Qt 并没有像 C++ 标准库那样明确地定义适配器类。然而,Qt 中的一些容器类可以被视为适配器,因为它们提供了对其他容器的封装,以实现特定的行为。例如:
QStack<T>
和QQueue<T>
:
- 这两个容器类可以被视为适配器,因为它们基于
QList<T>
实现了栈和队列的行为。**
QAbstractListModel
和QAbstractItemModel
:
- 这些类可以被视为适配器,因为它们为数据模型提供了一个通用的接口,可以用来绑定到各种 Qt 控件(如
QListView
和QTableView
)。
2.c 文件读取
文件的读取和写入
在C语言中,文件的读取和写入可以通过多个函数来实现,包括
fread
、fwrite
、fscanf
、fprintf
等函数。下面分别介绍这些函数的使用方法:
fread函数用于从文件中读取数据。它的声明如下:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
其中,
ptr
是一个指向数据存储位置的指针;size
是每个数据项的大小;count
是要读取的数据项数量;stream
是要读取的文件指针。fread
函数会从文件中读取指定数量的数据项,存储到指定位置,并返回实际读取的数据项数量。fwrite函数用于向文件写入数据。它的声明如下:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
其中,
ptr
是一个指向数据存储位置的指针;size
是每个数据项的大小;count
是要写入的数据项数量;stream
是要写入的文件指针。fwrite
函数会将指定位置的数据写入到文件中,并返回实际写入的数据项数量。fscanf函数用于从文件中按格式读取数据。它的声明如下:
int fscanf(FILE* stream, const char* format, ...);
其中,
stream
是要读取的文件指针;format
是格式字符串,用于指定要读取的数据的格式;...
是要读取的数据的地址。fscanf
函数会根据格式字符串的指定,从文件中读取数据,并将数据存储到指定的地址中。它返回成功匹配和读取的数据项数量。fprintf函数用于向文件按格式写入数据。它的声明如下:
int fprintf(FILE* stream, const char* format, ...);
其中,
stream
是要写入的文件指针;format
是格式字符串,用于指定要写入的数据的格式;...
是要写入的数据。fprintf
函数会根据格式字符串的指定,将数据按指定格式写入到文件中。它返回成功写入的字符数量。
(1)文本文件的读写操作:
1、文本文件的读取
在C语言中,可以使用
fgets
函数逐行读取文本文件,使用getc
函数逐字符读取文本文件。
fgets() 函数用于逐行读取文本文件。它的声明如下:
char* fgets(char* str, int n, FILE* stream);
其中,
str
是一个指向字符数组的指针,用于存储读取的字符串;n
是要读取的最大字符数(包括结尾的空字符);stream
是要读取的文件指针。fgets
函数会从文件中读取一行字符(包括换行符\n
),存储到指定的字符数组中,并在结尾添加一个空字符。它返回读取的字符串的指针,如果读取失败或到达文件结尾,则返回NULL
。getc() 函数用于逐字符读取文本文件。它的声明如下:
int getc(FILE* stream);
其中,
stream
是要读取的文件指针。getc
函数会从文件中读取一个字符,并返回读取的字符的ASCII码值(0-255)。如果到达文件结尾或读取失败,它会返回EOF
(End of File)。2、文本文件的写入
在C语言中,可以使用
fputs
函数逐行写入文本文件,使用putc
函数逐字符写入文本文件。
fputs函数用于逐行写入文本文件。它的声明如下:
int fputs(const char* str, FILE* stream);
其中,
str
是要写入的字符串;stream
是要写入的文件指针。fputs
函数会将指定的字符串写入到文件中,直到遇到结尾的空字符。它返回非负值表示成功,返回EOF
表示失败。putc函数用于逐字符写入文本文件。它的声明如下:
int putc(int ch, FILE* stream);
其中,
ch
是要写入的字符的ASCII码值(0-255);stream
是要写入的文件指针。putc
函数会将指定的字符写入到文件中。它返回写入的字符的ASCII码值(0-255),如果写入失败,返回EOF
。3、文本文件的格式化读写(格式化输入输出函数)
在C语言中,可以使用格式化输入输出函数来进行文本文件的格式化读写。常用的格式化输入函数有
fscanf
和fgets
,常用的格式化输出函数有fprintf
和fputs
。
fscanf函数用于从文本文件中进行格式化读取。它的声明如下:
int fscanf(FILE* stream, const char* format, ...);
其中,
stream
是要读取的文件指针;format
是格式化输入字符串,指定了读取数据的格式;...
表示可变参数,用于接收读取的数据。fscanf
函数会根据指定的格式从文件中读取数据,并将读取的数据存储到相应的变量中。它返回成功读取的数据个数。fprintf函数用于向文本文件中进行格式化写入。它的声明如下:
int fprintf(FILE* stream, const char* format, ...);
其中,
stream
是要写入的文件指针;format
是格式化输出字符串,指定了要写入的数据的格式;...
表示可变参数,用于传递要写入的数据。fprintf
函数会根据指定的格式将数据写入到文件中。它返回成功写入的字符数。
(2) 二进制文件的读写:
二进制文件操作
1、二进制文件的读取(按字节读取、按数据类型读取)
在C语言中,可以使用
fread
函数按字节读取二进制文件,使用fread
函数按数据类型读取二进制文件。
fread函数用于按字节读取二进制文件。它的声明如下:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
其中,
ptr
是要读取数据存储的内存地址;size
是每个数据项的字节数;count
是要读取的数据项的个数;stream
是要读取的文件指针。fread
函数会从文件中读取指定个数的数据项到指定的内存地址中。它返回实际成功读取的数据项个数。fread函数也可以按数据类型读取二进制文件,只需根据数据类型的字节数设置size参数,读取的数据项个数设置为1即可。例如,读取一个int类型的数据可以使用以下代码:
int num; fread(&num, sizeof(int), 1, fp);
2、二进制文件的写入(按字节写入、按数据类型写入)
在C语言中,可以使用
fwrite
函数按字节写入二进制文件,使用fwrite
函数按数据类型写入二进制文件。
fwrite函数用于按字节写入二进制文件。它的声明如下:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
其中,
ptr
是要写入的数据的内存地址;size
是每个数据项的字节数;count
是要写入的数据项的个数;stream
是要写入的文件指针。fwrite
函数会将指定个数的数据项从指定的内存地址写入文件中。它返回实际成功写入的数据项个数。fwrite函数也可以按数据类型写入二进制文件,只需根据数据类型的字节数设置size参数,写入的数据项个数设置为1即可。例如,写入一个int类型的数据可以使用以下代码:
int num = 123; fwrite(&num, sizeof(int), 1, fp);
(3)文件异常errno
常见的
errno
错误码包括:
EACCES
:权限不足
ENOENT
:文件不存在
EEXIST
:文件已存在
ENOMEM
:内存不足
EBADF
:无效的文件描述符
EIO
:IO错误
EINVAL
:无效的参数
EPIPE
:管道破裂
QT中使用:
QFile file("example.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "无法打开文件:" << file.errorString();
} else {
// 文件成功打开,可以进行读写操作
// ...
file.close();
}
2024.08.13
1.补昨天的记录:七种单例模式的实现
饿汉式 以及 懒汉式:区别在于static Singleton instance 对象是否在程序初始化就存在
class Singleton {
private:
Singleton() {}
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;
public:
static Singleton* getInstance() {
// C++11线程安全的局部静态变量初始化
static Singleton instance;
return &instance;
}
};
饿汉式比较实用:因为
C++中,使用局部静态变量实现单例模式是一种常见且简洁的方式。
使用局部静态变量实现单例模式的优点在于代码简洁,且在多线程环境下是线程安全的。不需要手动处理线程同步问题,C++ 编译器会自动确保静态局部变量只被初始化一次。
解释一下上述说法:
在这个示例中,getInstance()
函数中的 static Singleton instance;
是一个局部静态变量。当第一次调用 getInstance()
时,局部静态变量 instance
将被初始化。由于局部静态变量的初始化是由C++11标准库保证线程安全的,因此不需要程序员手动添加任何同步代码(如互斥锁)。
不同于懒汉式:第一次调用getInstance()时才会初始化
class Singleton {
private:
static Singleton* instance;
static std::mutex mutex;
Singleton() {}
Singleton(const Singleton& other) {}
Singleton& operator=(const Singleton& other) {}
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex); // 加锁
if (instance == nullptr) { // 第二次检查
instance = new Singleton(); // 创建实例
}
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
懒汉式的双检锁机制通过在加锁前后进行两次检查,避免了大部分情况下的锁开销,提高了性能。同时,使用互斥锁保证了在多线程环境下只有一个线程能够进入关键代码段。
双检锁机制
双检锁机制通过两次检查来减少锁的使用:
第一次检查(无锁):在尝试访问对象之前,首先检查该对象是否已经被初始化。如果已经被初始化,那么就可以直接安全地使用它,而无需进行加锁操作。
加锁:如果第一次检查发现对象还未被初始化,那么线程会尝试获取一个锁(比如使用互斥锁)。这个锁的作用是确保在同一时间只有一个线程能够进入这个“检查并初始化”的代码块。
第二次检查(有锁):在获取锁之后,线程会再次检查对象是否已经被其他线程初始化了(因为在获取锁之前可能已经有其他线程完成了初始化)。这是为了避免重复初始化。
初始化:如果确认对象还未被初始化,那么就在锁的保护下进行初始化。
释放锁:完成初始化后,释放锁,以便其他线程可以访问这个已经被初始化的对象。
智能指针形式:
/*
* 智能指针的单例实现
* */
class SingleTon03{
private:
SingleTon03(){}
public:
static shared_ptr<SingleTon03> getInstance(){
static shared_ptr<SingleTon03> instance(new SingleTon03());
return instance;
}
};
2024.08.09
接昨天的多线程问题:
首先提出几个问题:
线程同步之后,输出结果顺序输出,那么这不和串行效果一样吗?
多线程中的同步处理确实与串行执行在某些方面看起来相似,因为它们都涉及到对执行顺序的控制,但它们的根本目的和效果是不同的。多线程的引入主要是为了利用多核处理器的并行处理能力,以提高程序的执行效率和响应速度。然而,多线程编程也带来了新的问题,特别是当多个线程需要访问共享资源(如内存中的数据)时,就可能发生竞态条件(race condition)和数据不一致等问题。这就是为什么需要同步处理的原因。
-
防止竞态条件:
竞态条件是指两个或多个线程在访问共享资源时,由于执行顺序的不确定性而导致的结果不可预测。同步机制可以确保在任一时刻只有一个线程能访问共享资源,从而避免竞态条件的发生。 -
保持数据一致性:
当多个线程修改同一数据时,如果没有适当的同步,可能会导致数据被错误地覆盖或损坏,
同步与串行的区别
-
执行效率:
串行执行是按顺序一个接一个地执行任务,而多线程执行可以并行处理多个任务。即使使用了同步,多线程通常也能比完全串行执行更快地完成任务,尤其是在多核处理器上。 -
资源利用率:
多线程可以更充分地利用系统资源(如CPU、内存和I/O设备),因为不同的线程可以在等待某些资源(如I/O操作)时,执行其他任务。 -
编程复杂性:
多线程编程比串行编程更复杂,因为需要处理线程间的同步、通信和并发问题。然而,通过合理的同步策略和设计模式,可以降低这种复杂性。
二、explicit的用法
在 C++ 中,explicit 关键字用于限制构造函数的隐式转换。当声明一个带有 explicit 关键字的单参数构造函数时,它就不能被用于隐式的类型转换。这对于防止意外的类型转换非常有用,特别是在类的设计中,可以避免因构造函数被误用而导致的潜在问题。
隐式构造 vs 显式构造
隐式构造:是指在不需要显式调用构造函数的情况下,编译器自动进行的类型转换。例如,从一个类型到另一个类型的自动转换。
显式构造:是指显式调用构造函数来创建对象的过程。这通常是通过构造函数名后面跟括号 () 或初始化列表 {} 来完成的。
int main()
{
// 显式构造
MyClass obj1(10);
obj1.printData(); // 输出: Data: 10
// 隐式构造将导致编译错误
// MyClass obj2 = 20; // 错误: explicit 关键字禁止了隐式转换
// 显式构造
MyClass obj3(30);
obj3.printData(); // 输出: Data: 30
return 0;
}
google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换,缺点无。所以google约定所有单参数的构造函数都必须是显示的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。(缺点无 不能再同意了)
effective c++中说:被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。
足见explicit 的重要性!!!
2024.08.08
断更两天,反思反思,严格来说断更了一天,7号学习了c++的异常处理
一、异常的抛出与处理
C++异常的抛出和捕获是通过使用"throw
"和"try-catch
"语句来实现的。
语法:
try{
throw 异常值;
}
catch(异常类型1 异常值1)
{
// 处理异常的代码
}
catch(异常类型2 异常值2)
{
// 处理异常的代码
}
catch(...)// 任何异常都捕获
{
// 处理异常的代码
}
常见的c++异常:
二、线程池的实现
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
std::condition_variable cond;
std::mutex mtx;
std::atomic<bool> running{true};
std::queue<int> q;
void threadFunc() {
while (running.load()) {
std::unique_lock<std::mutex> ltx(mtx);
cond.wait(ltx, [] { return !q.empty() || !running.load(); });
if (!q.empty()) {
int param = q.front();
q.pop();
std::cout << "param: " << param << std::endl;
}
if (!running.load()) {
break; // 如果不再运行,则退出循环
}
}
}
void jobDispatch() {
for (int i = 0; i < 1000; ++i) {
q.push(i);
}
cond.notify_all(); // 唤醒所有等待的线程
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待所有任务完成
running.store(false); // 标记为不再运行
cond.notify_all(); // 再次唤醒所有线程以确保它们都能退出
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(threadFunc);
}
jobDispatch();
for (auto& th : threads) {
th.join();
}
return 0 ;
}
流程讲解:
程序流程概述
-
初始化: 程序开始时,初始化全局变量
running
为true
,表示线程可以继续运行。同时,创建一个空的队列q
用于存储任务。 -
创建线程: 在
main
函数中,创建多个工作线程,每个线程都会执行threadFunc
函数。 -
线程执行: 每个工作线程执行
threadFunc
函数,进入一个无限循环,检查队列是否为空或running
是否为false
。 -
任务分发: 在
jobDispatch
函数中,向队列中添加任务,并通知所有等待的线程。 -
任务处理: 工作线程处理队列中的任务,直到队列为空或
running
变为false
。 -
停止线程: 当所有任务处理完毕后,设置
running
为false
,并通过notify_all
唤醒所有等待的线程,使其退出循环。 -
线程结束: 工作线程退出循环后,等待它们结束并回收资源。
详细流程
1. 初始化
- 全局变量
running
被初始化为true
。 - 创建一个空的队列
q
。
2. 创建线程
- 在
main
函数中,使用std::vector<std::thread>
存储所有线程。 - 循环创建 10 个工作线程,每个线程执行
threadFunc
函数。
3. 线程执行
- 每个工作线程执行
threadFunc
函数,进入一个无限循环。 - 线程获取锁
mtx
,并调用cond.wait
,等待队列非空或running
变为false
。 - 如果队列非空,线程处理队列中的任务。
- 如果
running
为false
,线程退出循环。
4. 任务分发
- 在
jobDispatch
函数中,向队列q
中添加 1000 个任务。 - 调用
cond.notify_all()
唤醒所有等待的线程。
5. 任务处理
- 线程被唤醒后,检查队列是否非空。
- 如果队列非空,处理队列中的任务。
- 如果队列为空,线程继续等待。
6. 停止线程
- 在所有任务添加到队列后,主线程等待一段时间,确保所有任务被处理。
- 设置
running
为false
。 - 再次调用
cond.notify_all()
唤醒所有线程。
7. 线程结束
- 线程检测到
running
为false
后退出循环。 - 主线程等待所有工作线程结束。
- 程序结束。
注意两个问题:
1.wait
被放在条件检查 !q.empty()
之后。这种做法存在以下问题:
-
无效等待: 如果
q.empty()
为true
,则wait
将会被调用,等待队列变为非空。然而,在调用wait
之前,已经有了一次!q.empty()
的检查,这意味着如果队列为空,wait
会在检查队列为空之后立即被调用。这样的设计可能导致线程在队列为空时反复检查队列状态,而不是真正地等待。 -
条件竞争: 如果线程在检查队列是否为空之后,但在调用
wait
之前被调度出去,此时其他线程可能已经添加了元素到队列中,使得队列非空。当原线程重新获得 CPU 并调用wait
时,它可能会错过队列已变非空的状态。
2. 这里为什么需要添加?
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待所有任务完成
running.store(false); // 标记为不再运行
cond.notify_all(); // 再次唤醒所有线程以确保它们都能退出
为什么需要这些步骤:
-
确保所有任务完成:
- 因为线程可能正在处理队列中的任务,我们需要给它们足够的时间来完成任务。如果没有这一步,主线程可能会在任务还未完全处理完的情况下就设置
running
为false
,从而导致任务未完成的情况发生。
- 因为线程可能正在处理队列中的任务,我们需要给它们足够的时间来完成任务。如果没有这一步,主线程可能会在任务还未完全处理完的情况下就设置
-
设置
running
为false
:- 这是为了通知所有的工作线程停止处理任务。如果不这样做,线程可能会无限期地运行下去。
-
再次唤醒所有线程:
- 即使所有任务都已经完成,仍然有可能有线程处于等待状态,等待队列非空。为了确保所有线程都能接收到停止信号,我们需要再次调用
notify_all
来唤醒它们。这是因为即使队列为空,也可能有线程正在等待队列变为非空,而它们需要检查running
是否为false
。
- 即使所有任务都已经完成,仍然有可能有线程处于等待状态,等待队列非空。为了确保所有线程都能接收到停止信号,我们需要再次调用
这里为什么需要添加
2024.08.05
1.extern "C"用法:
头文件func.h
#if __cplusplus
extern "C"{
#endif
int func(int a,int b);
#if __cplusplus
}
#endif
#include <stdio.h>
#include "func.h"
int func(int a,int b){
printf("c func\n");
return a+b;
}
#include "func.h"
int main()
{
/*2024.08.05
* */
func(100,200);
return 0;
}
2. 内联函数、函数重载
/*
* 2024.08.05 内联函数 函数重载
* 内联函数声明时不要加inline,定义时必须加inline
* 不能存在任何形式的循环语句以及大量的条件判断语句
* */
int mAdd(int x,int y);
inline int mAdd(int x,int y){
return x+y;
}
/*
* 函数重载:同一作用域参数的类型、个数、顺序不同
* 为什么函数返回值不能作为重载条件?
* 这是因为函数调用时实参和形参的匹配过程是在编译期完成的,
* 而编译器无法确定一个表达式的返回类型,
* 因此不能以返回值类型作为重载条件。
*
* 原理:编译器会将函数名和参数列表一起进行名称修饰,生成一个新的、
* 唯一的符号名称,这个符号名称就是该函数在目标代码中的标识。
* */
void func(int x){
cout<<"int"<<endl;
}
void func(char x){
cout<<"char"<<endl;
}
void func(char x,int y){
cout<<"char int"<<endl;
}
void func(int y,char x){
cout<<"int char"<<endl;
}
2024.08.01
come on ,坚持住
类模板:类模板一般在hpp
文件里面实现。由于数组类模板要存放任何数据类型,所以先定义一个模板类型T
。
模仿vector容器实现自定义数组类
#pragma once
#ifndef _DATA_H_
#include <string.h>
#include <iostream>
using namespace std;
template <class T>
class MyArray{
template<class T1>
friend ostream& operator<<(ostream &out,MyArray<T1> ob);
private:
T* arr;
int size;
int capacity;
public:
MyArray();
MyArray(int capacity);
MyArray(const MyArray &ob);
~MyArray();
MyArray& operator=(MyArray& ob);
void pushBack(T elem);
void sortArray();
};
#endif //!_DATA_H
template<class T>
MyArray<T>::MyArray(){
capacity =5;
size =0;
arr = new T[capacity];
memset(arr, 0, sizeof(T)*capacity);
}
template<class T>
MyArray<T>::MyArray(int capacity){
this->capacity = capacity;
size =0;
arr = new T[capacity];
memset(arr,0,sizeof(T)*capacity);
}
template<class T>
MyArray<T>::MyArray(const MyArray& ob){
this->capacity= ob.capacity;
this->size = ob.size;
this->arr = new T[this->capacity];
memset(this->arr, 0, sizeof(T)*this->capacity);
memcpy(this->arr,ob.arr,sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::~MyArray(){
if(arr!=NULL){
delete[] arr;
arr = NULL;
}
}
template<class T>
MyArray<T>& MyArray<T>::operator=(MyArray<T>& ob){
if(arr != NULL ){
delete[] arr;
arr=NULL;
}
this->capacity = ob.capacity;
this->size = ob.size;
this->arr = new T[this->capacity];
memset(this->arr, 0, sizeof(T)*this->capacity);
return *this;
}
template<class T>
void MyArray<T>::pushBack(T elem){
if(size == capacity){
capacity = 2*capacity;
T*tmp = new T[capacity];
if (arr!= NULL){
memcpy(tmp, arr, sizeof(T)*size);
delete[] arr;
}
arr = tmp;
}
arr[size] = elem;
size++;
}
template <class T>
void MyArray<T>::sortArray(){
if (size ==0){
cout<<"容器没有数据"<<endl;
return;
}
for(int i=0;i<size-1;i++){
for(int j=0;j<size-i-1;j++){
if(arr[j]>arr[j+1]){
T tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
template <class T1>
ostream& operator<<(ostream& out, MyArray<T1>ob){
for(int i=0;i<ob.size;i++){
out<<ob.arr[i]<<" ";
}
out<<endl;
return out;
}
main调用:
/*2024.08.01类模板 */
MyArray<int>arr01;
arr01.pushBack(20);
arr01.pushBack(60);
arr01.pushBack(30);
cout<<arr01<<endl;
arr01.sortArray();
cout<<arr01<<endl;
MyArray<char>arr02;
arr02.pushBack('A');
arr02.pushBack('g');
arr02.pushBack('b');
cout<<arr02<<endl;
arr02.sortArray();
cout<<arr02<<endl;
2024.07.31
友元类、友元函数:可以访问另一个类的私有变量。
/*
* 2024.07.31 友元函数
* */
class Room;//说明类名称
class Good{
public:
int visiting01(Room &room);
int visiting02(Room &room);
};
class Room{
friend class Good;
friend int visiting(Room &room);
private:
string bedRoom;
public:
string settingRoom;
public:
Room(string bedRoom, string setingRoom){
this->bedRoom = bedRoom;
this->settingRoom = setingRoom;
}
};
int visiting(Room &room){
cout<<room.settingRoom<<endl;//友元函数可访问私有成员变量
cout<<room.bedRoom << endl;
return 0;
}
int Good::visiting01(Room &room){
cout<< room.settingRoom <<endl;
cout<< room.bedRoom <<endl;
return 0;
}
int main()
{
Room room("Bed", "seting");
visiting(room);
Good good;
good.visiting01(room);
good.visiting02(room);
return 0;
}
2024.07.30
1. c++类模版
首先给几个结论:
类模板的定义通常需要放在头文件中,以便在使用时能够正确地进行编译和链接
//template <class T1,class T2> //或者使用typename代替
template <typename T1, typename T2>
class Data{
//函数模板作为类模板的友元
template <typename T3, typename T4>
friend void MyPrint(Data<T3, T4> &ob);
private:
T1 a;
T2 b;
public:
Data(){
cout<<" Data 的无参构造" <<endl;
}
Data(T1 a , T2 b){
this->a = a ;
this->b = b;
cout<< " Data 的有参构造"<<endl;
}
void showData(){
cout << a <<" "<< b<<endl;
}
};
//函数模板作为类模板的定义
template <typename T3, typename T4>
void MyPrint(Data<T3, T4> &ob){
cout<<"函数模板友元:" << ob.a<<" " << ob.b<<endl;
}
int main()
{
/*2024.07.30类模板
*
* */
//实例化对象必须给出类型T,Dta为类模板
Data<int,int> ob(300,400);
//ob.showData();
MyPrint(ob);
Data<int ,char> ob2(100, 'A');
//ob2.showData();
MyPrint(ob2);
return 0;
}
额外知识点:
hpp: 头文件一般不包含具体实现的内容,所以为了区分,C++使用了一种特殊的头文件名称.hpp(.h和.cpp的结合体)。
2.死锁
void* thread_a(void* arg) {
pthread_mutex_lock(&mutex_x);
// do something with resource X
pthread_mutex_lock(&mutex_y);
// do something with resource Y
pthread_mutex_unlock(&mutex_y);
pthread_mutex_unlock(&mutex_x);
}
void* thread_b(void* arg) {
pthread_mutex_lock(&mutex_x); // 注意这里先获取了资源 X 的锁
pthread_mutex_lock(&mutex_y);
// do something with resource Y
// do something with resource X
pthread_mutex_unlock(&mutex_x);
pthread_mutex_unlock(&mutex_y);
}
线程 A 和线程 B 都按照相同的顺序获取锁,即先获取资源 X 的锁再获取资源 Y 的锁。这样就能够避免死锁问题。反之:就会出现死锁:如果线程 A 先锁定了资源 X,然后尝试获取资源 Y,同时线程 B 先锁定了资源 Y,然后尝试获取资源 X,就会导致死锁问题。
void* thread_a(void* arg) {
pthread_mutex_lock(&mutex_x);
// do something with resource X
pthread_mutex_lock(&mutex_y);
// do something with resource Y
pthread_mutex_unlock(&mutex_y);
pthread_mutex_unlock(&mutex_x);
}
void* thread_b(void* arg) {
pthread_mutex_lock(&mutex_y);
// do something with resource Y
pthread_mutex_lock(&mutex_x);
// do something with resource X
pthread_mutex_unlock(&mutex_x);
pthread_mutex_unlock(&mutex_y);
}
2024.07.29
今天学习:Lamda 表达式
Lambda 表达式的语法
Lambda 表达式的语法如下:
1[capture-clause] (parameters) mutable [exception-specification] -> return-type { function-body }
[capture-clause]
:指定 lambda 如何捕获外围作用域中的变量。可以是按值捕获或按引用捕获。(parameters)
:lambda 函数的参数列表。mutable
:可选关键字,用于指定 lambda 表达式可以修改通过引用或按值捕获的变量。[exception-specification]
:可选的异常规范。-> return-type
:可选的返回类型说明符,可以省略。{ function-body }
:lambda 函数体。
Lambda 表达式的捕获方式
Lambda 表达式可以通过两种方式捕获变量:
- 按值捕获:将变量的副本放入 lambda 内部的闭包中。
- 按引用捕获:将变量的引用放入 lambda 内部的闭包中。
默认情况下,所有在 lambda 表达式中使用的变量都是按值捕获的。如果想按引用捕获,需要显式指定。
int x=10;
auto add_five= [x](){return x+5;};
//修改捕获的变量
auto increment =[x]() mutable {x++;return x;};
std::cout<<increment()<<endl;
//带参函数表达
std::vector<int> v = {1,2,3,4,6,5};
std::sort(v.begin(),v.end(), [](int a,int b){return a > b;});
cout<< "Sorted in reverse order: ";
for (int i: v){
std::cout<< i <<" ";
}
std::cout<< std::endl;
2024.07.26
一个表格
特性 | 重载(Overloading) | 重写(Overriding) | 重定义(Redefining)/隐藏(Hiding) |
---|---|---|---|
发生位置 | 同一作用域(如类内部) | 继承关系中(子类与父类之间) | 继承关系中(子类与父类之间) |
函数名 | 相同 | 相同 | 相同 |
参数列表 | 不同 | 必须相同 | 可以不同 |
返回值类型 | 无要求 | 可以不同(但必须是兼容的) | 可以不同 |
多态性 | 编译时多态 | 运行时多态 | 不触发多态 |
关键字(C++) | 无 | virtual(父类中) | 无 |
关键点 | 同一作用域内,函数名相同,参数列表不同 | 继承关系,函数名、参数列表相同,返回值兼容,父类函数需为virtual | 继承关系,函数名相同,参数和返回值可不同,不触发多态 |
//2024.07.26
/*
*
* 重载与重写
* */
class MyClass{
public:
void myFunction(int x){
cout<<"重载"<<x<<endl;
}
void myFunction(double x){
cout<<"编译时多态,静态多态"<<x<<endl;
}
};
class Base{
public:
virtual void myFunction(){
cout<<"基类虚函数"<<endl;
}
};
class Derived : public Base{
public:
void myFunction() override{
cout<<"子类虚函数重写,运行时多态"<<endl;
}
};
int main()
{
//2024.07.26重写
Base* obj = new Derived();
obj->myFunction();
//重载
MyClass* ob = new MyClass;
ob->myFunction(2.0);
ob->myFunction(1);
return 0;
}
实践证明:觉得看懂的东西,其实离真正懂还是有一段距离的!继续坚持,少年,不.... 仍是少年
2024.07.25
1.很开心,坚持了第三天
继承:所有父类的私有数据在子类中不可访问,公共继承保持不必,保护继承变保护,私有继承变私有。
(1)
因为:cout << ob.b << ob.a << endl;
这行代码会导致编译错误,因为b
虽然可以在B类的成员函数内部访问,但不能从B类的外部直接访问。想要访问b,需要在B内部调用才可以:
//2024.07.25
class A{
private:
int a;
protected:
int b;
public:
int c;
};
class B: public A{
public:
void func(void){
cout<<b<<c<<endl;
}
int getB() const{
return b;
}
};
2.继承:成员初始化顺序:
class A{
public:
int a;
protected:
int b;
A(int a){
cout<<"A有参构造函数"<<endl;
this->a = a;
}
~A(){
cout<<"A 析构函数"<<endl;
}
public:
int c;
};
class Other{
public:
int b;
public:
Other(){
cout<<"Other 无参构造函数"<<endl;
}
Other(int b){
cout<<"Other 有参构造"<<endl;
this->b = b;
}
~Other(){
cout<<"Other析构函数"<<endl;
}
};
class B: public A{
private:
Other ot;
public:
int a;
public:
B(int a,int b,int c):ot(b),A(a){
cout<<"B 有参构造函数"<<endl;
this->a = a;
}
void func(void){
cout<<b<<c<<endl;
}
int getB() const{
return b;
}
~B(){
cout<<"B 析构函数"<<endl;
}
};
int main()
{
//2024.07.25
B ob(100,200,300);
//cout<<ob.b<<ob.a<<endl;
//cout<<ob.getB()<<endl;
cout<<ob.a<<" "<<ob.A::a<<endl;
}
(1)即使初始化列表中的对像构造函数调用顺序:先基类构造,再成员对象构造;如果是多个成员对象,则根据定义时的顺序而定,并不是根据初始化列表中出现的顺序。
(2)注意;子类和父类具有相同成员名,子类优先使用自己的成员变量或者成员函数,使用父类的需要加::作用域
2024.07.24
1.` 初始化列表的使用:类像调用对象成员的有参构造,必须使用初始化列表
class A{
public:
int mA;
public:
A(){
cout<<"A 无参构造"<<endl;
}
A(int num){
mA = num;
cout<<"A 有参构造: "<<mA<<endl;
}
~A(){
cout<<"A 析构函数"<<endl;
}
};
class B{
public:
int mB;
A mA;
public:
B(){
cout<<"B 无参构造"<<endl;
}
B(int num){
mA.mA = num;
mB = num;
cout<<"B 有参构造"<<endl;
}
//初始化列表
B(int a,int b):mA(a){
mB = b;
cout<<"B 有参构造:"<<mB<<endl;
}
};
/*class Person{
public:
char* name;
int age;
Person(int num,const char* str){
age = num;
name = new char[strlen(str)+1];
strcpy(name,str);
}
//拷贝构造
Person(const Person& p){
name = new char[strlen(p.name)+1];
strcpy(name,p.name);
age = p.age;
}
~Person(){
if(name!= NULL)
delete [] name;
}
};*/
int main()
{
//2024.07.24
{
B ob1;
}
cout<<"------------------------"<<endl;
{
B ob2(100);
}
cout<<"-------------------"<<endl;
{
B ob3(200,300);
}
3. 深拷贝
#include <iostream>
#include <string.h>
using namespace std;
class Person{
public:
char* name;
int age;
Person(int num,const char* str){
age = num;
name = new char[strlen(str)+1];
strcpy(name,str);
}
//拷贝构造
Person(const Person& p){
name = new char[strlen(p.name)+1];
strcpy(name,p.name);
age = p.age;
}
~Person(){
if(name!= NULL)
delete [] name;
}
};
int main()
{
//2024.07.24
Person person1(18,"Tom");
//深拷贝
Person person2 = person1;
cout<<"Person1: "<<person1.name<<", "<<person1.age<<endl;
cout<<"Person2: "<<person2.name<<", "<<person2.age<<endl;
return 0;
}
2024.07.23
还是断更了,坚持真的是一件很难的事!
2024.07.15
bug1:
#include <iostream>
#include <string.h>
class Person{
public:
char name[32];
int age;
Person(){
strcpy(name , '\0');//直接赋值会报错,不支持指定数组类型
age= 0;
}
Person(const char* n, int a){
strncpy(name,n,sizeof(name)-1);
name[sizeof(name)-1] = '\0';
age = a;
}
};
int main()
{
//2024.07.15
Person p1;
Person p2 = Person();
Person p3("Tom",25);
Person p4 = Person("Lion",18);
Person p5{"Jerry",30};//c++11 以后比较受欢迎
Person();
Person("Long",20);
return 0;
}
一开始是 name =" "; 结果报错,数组名不可以直接赋值字符串,需要使用strpy函数
bug2:Data ob;//报错,实现了拷贝构造,编译器不自动生成默认的构造函数。
#include <iostream>
#include <string.h>
using namespace std;
/*
* 析构*
* */
class Data{
public:
int a;
char* name;
public:
// Data(){
// a = 100;
// cout<<"无参构造函数"<<endl;
// }
Data(int p){
a = p;
cout<<"有参构造函数"<<a<<endl;
}
Data(int p,const char* str){
a = p;
name = new char[strlen(str)+1];
strcpy(name,str);
cout<<"有参构造函数"<<a<<", "<<name<<endl;
}
//拷贝构造
Data(const Data& ob){
a = ob.a;
}
~Data(){
cout<<"析构函数"<<a<<endl;
if(name!=NULL)
delete[] name;
}
};
int main()
{
Data ob;
Data ob01(200,"Lion");
cout<<ob01.name<<endl;
Data ob02 = ob01;
return 0;
}
所以;以后一般都确保你的类中将所有的构造函数都添加了,避免出现编译错误
记:实现了有参构造,编译器不自动生成默认的无参构造函数。
实现了有参构造或无参构造函数,不影响编译器自动生成默认的拷贝构造函数。
2024.07.16
知识点:拷贝构造和赋值运算符有什么区别?
// 拷贝构造函数
Data(const Data& ob) : a(ob.a), name(nullptr) {
if (ob.name != nullptr) {
name = new char[strlen(ob.name) + 1];
strcpy(name, ob.name);
}
}
// 拷贝赋值运算符
Data& operator=(const Data& ob) {
if (this != &ob) {
a = ob.a;
char* new_name = nullptr;
if (ob.name != nullptr) {
new_name = new char[strlen(ob.name) + 1];
strcpy(new_name, ob.name);
}
delete[] name;
name = new_name;
}
return *this;
}
int main() {
Data obj1(10, "Object1");
Data obj2(obj1); // 使用拷贝构造函数创建 obj2,它将是 obj1 的副本
// Data obj2 = obj1;//不推荐这种写法
return 0;
}
以上为拷贝构造,建立一个新的对象。
// Data obj2 = obj1;//不推荐这种写法
int main() {
Data obj1(10, "Object1");
Data obj2(20, "Object2");
obj2 = obj1; // 使用拷贝赋值运算符将 obj1 的值赋给 obj2
return 0;
}
以上为赋值运算符,对已有对象赋值