欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > C++复习

C++复习

2025/11/9 20:23:09 来源:https://blog.csdn.net/xwy13886467077/article/details/147767394  浏览:    关键词:C++复习

线程库(类)

在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。

thread的带参的构造函数的定义如下:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

线程类的使用: 

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t3 = thread(func, 10);t3.join();return 0;
}
  • 如果创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行。
  • thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
  • 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:

方式一:借助std::ref函数

当线程函数的参数类型为引用类型时,如果要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref函数保持对实参的引用。比如:

void add(int& num)
{num++;
}
int main()
{int num = 0;thread t(add, ref(num));t.join();cout << num << endl; //1return 0;
}

线程回收:join与,ref值引用

启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。

主线程创建新线程后,可以调用join函数等待新线程终止,当新线程终止时join函数就会自动清理线程相关的资源。join函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join,否则程序会崩溃。比如:

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t(func, 20);//  t.detach(); 主线程直接分离线程,不再阻塞t.join();t.join(); //程序崩溃return 0;
}

因此采用join方式结束线程时,join的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,也就是利用对象的生命周期来控制线程资源的释放。比如:

class myThread
{
public:myThread(thread& t):_t(t){}~myThread(){if (_t.joinable())_t.join();}//防拷贝myThread(myThread const&) = delete;myThread& operator=(const myThread&) = delete;
private:thread& _t;
};

互斥量库(mutex)

mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝,也不能进行移动。

void func(int n, mutex& mtx)
{mtx.lock(); //for循环体外加锁for (int i = 1; i <= n; i++){//mtx.lock(); //for循环体内加锁cout << i << endl;//mtx.unlock();}mtx.unlock();
}
int main()
{mutex mtx;thread t1(func, 100, ref(mtx));thread t2(func, 100, ref(mtx));t1.join();t2.join();return 0;
}

锁资源和线程资源都需要我们进行管理。都可以使用RAII风格管理。lock_guard和unique_lock

mutex mtx;
void func()
{//...//匿名局部域{lock_guard<mutex> lg(mtx); //调用构造函数加锁FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //调用析构函数解锁}} //调用析构函数解锁//...
}
int main()
{func();return 0;
}

如下场景就适合使用unique_lock:

  • 要用互斥锁保护函数1的大部分代码,但是中间有一小块代码调用了函数2,而调用函数2时不需要用函数1中的互斥锁进行保护,函数2内部的代码由其他互斥锁进行保护。
  • 因此在调用函数2之前需要对当前互斥锁进行解锁,当函数2调用返回后再进行加锁,这样当调用函数2时其他线程调用函数1就能够获取到这个锁。

原子类库

原子类解决线程安全问题

C++11中引入了原子操作类型,使得线程间数据的同步变得非常高效。

  • 为了防止意外,标准库已经将atomic模板类中的拷贝构造、移动构造、operator=默认删除掉了。
  • 原子类型不仅仅支持原子的++操作,还支持原子的--、加一个值、减一个值、与、或、异或操作。

如下:两个线程对同一个共享变量进行++操作

void func(atomic_int& n, int times)
{for (int i = 0; i < times; i++){n++;}
}
int main()
{atomic_int n = { 0 };int times = 100000; //每个线程对n++的次数thread t1(func, ref(n), times);thread t2(func, ref(n), times);t1.join();t2.join(); //先join等两个子线程执行完毕cout << n << endl; //打印n的值return 0;
}

实现两个线程交替打印1-100

尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

int main()
{int n = 100;mutex mtx;condition_variable cv;bool flag = true;//奇数thread t1([&]{int i = 1;while (i <= 100){unique_lock<mutex> ul(mtx);cv.wait(ul, [&flag]()->bool{return flag; }); //等待条件变量满足cout << this_thread::get_id() << ":" << i << endl;i += 2;flag = false;cv.notify_one(); //唤醒条件变量下等待的一个线程}});//偶数thread t2([&]{int j = 2;while (j <= 100){unique_lock<mutex> ul(mtx);cv.wait(ul, [&flag]()->bool{return !flag; }); //等待条件变量满足cout << this_thread::get_id() << ":" << j << endl;j += 2;flag = true;cv.notify_one(); //唤醒条件变量下等待的一个线程}});t1.join();t2.join();return 0;
}

C++异常

异常是面向对象语言常用的一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数直接或间接的调用者处理这个错误。

  • throw:当程序出现问题时,可以通过throw关键字抛出一个异常。
  • try:try块中放置的是可能抛出异常的代码,该代码块在执行时将进行异常错误检测,try块后面通常跟着一个或多个catch块。
  • catch:如果try块中发生错误,则可以在catch块中定义对应要执行的代码块。

异常的抛出和捕获的匹配原则:

  • 异常是通过throw 对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码,如果抛出的异常对象没有捕获,或是没有匹配类型的捕获,那么程序会终止报错。
  • 被选中的处理代码(catch块)是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(类似于函数的传值返回)
  • catch(...)可以捕获任意类型的异常,但捕获后无法知道异常错误是什么。
  • 实际异常的抛出和捕获的匹配原则有个例外,捕获和抛出的异常类型并不一定要完全匹配,可以抛出派生类对象,使用基类进行捕获,这个在实际中非常有用。

最基础的异常类至少需要包含错误编号和错误描述两个成员变量,甚至还可以包含当前函数栈帧的调用链等信息。该异常类中一般还会提供两个成员函数,分别用来获取错误编号和错误描述。比如:

class Exception
{
public:Exception(int errid, const char* errmsg):_errid(errid), _errmsg(errmsg){}int GetErrid() const{return _errid;}virtual string what() const{return _errmsg;}
protected:int _errid;     //错误编号string _errmsg; //错误描述//...
};

其他模块如果要对这个异常类进行扩展,必须继承这个基础的异常类,可以在继承后的异常类中按需添加某些成员变量,或是对继承下来的虚函数what进行重写,使其能告知程序员更多的异常信息。异常类的成员变量不能设置为私有,因为私有成员在子类中是不可见的,基类Exception中的what成员函数最好定义为虚函数,方便子类对其进行重写,从而达到多态的效果。

智能指针

智能指针的原理

实现智能指针时需要考虑以下三个方面的问题:

  1. 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
  2. *->运算符进行重载,使得该对象具有像指针一样的行为。
  3. 智能指针对象的拷贝问题。浅拷贝析构两次

auto_ptr管理权转移

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。

 unique_ptr防拷贝

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放

shared_ptr 引用计数

类模板,shared_ptr<int> p(new int(0));

namespace xwy
{template<class T>class shared_ptr{private://++引用计数void AddRef(){_pmutex->lock();(*_pcount)++;_pmutex->unlock();}//--引用计数void ReleaseRef(){_pmutex->lock();bool flag = false;if (--(*_pcount) == 0) //将管理的资源对应的引用计数--{if (_ptr != nullptr){cout << "delete: " << _ptr << endl;delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;flag = true;}_pmutex->unlock();if (flag == true){delete _pmutex;}}public://RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmutex(new mutex){}~shared_ptr(){ReleaseRef();}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _pmutex(sp._pmutex){AddRef();}shared_ptr& operator=(shared_ptr<T>& sp){if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作{ReleaseRef();         //将管理的资源对应的引用计数--_ptr = sp._ptr;       //与sp对象一同管理它的资源_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数_pmutex = sp._pmutex; //获取sp对象管理的资源对应的互斥锁AddRef();             //新增一个对象来管理该资源,引用计数++}return *this;}//获取引用计数int use_count(){return *_pcount;}//可以像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;        //管理的资源int* _pcount;   //管理的资源对应的引用计数mutex* _pmutex; //管理的资源对应的互斥锁};
}
std::shared_ptr的定制删除器

定制删除器的用法

第二个参数传入一个 仿函数,再智能指针析构时调用

template<class T>
struct DelArr
{void operator()(const T* ptr){cout << "delete[]: " << ptr << endl;delete[] ptr;}
};
int main()
{std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){cout << "fclose: " << ptr << endl;fclose(ptr);});return 0;
}

weak_ptr解决循环引用

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。比如:

struct ListNode
{std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;//...cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}

C语言中的类型转换

C语言中有两种形式的类型转换,分别是隐式类型转换和显式类型转换:

  • 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
  • 显式类型转换:需要用户自己处理,以(指定类型)变量的方式进行类型转换。

需要注意的是,只有相近类型之间才能发生隐式类型转换,比如int和double表示的都是数值,只不过它们表示的范围和精度不同。而指针类型表示的是地址编号,因此整型和指针类型之间不会进行隐式类型转换,如果需要转换则只能进行显式类型转换。比如:

int main()
{//隐式类型转换int i = 1;double d = i;cout << i << endl;cout << d << endl;//显式类型转换int* p = &i;int address = (int)p;cout << p << endl;cout << address << endl;return 0;
}

int a = xxx_cast<int> (b);

static_cast

static_cast用于相近类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关类型之间转换。比如:

int main()
{double d = 12.34;int a = static_cast<int>(d);cout << a << endl;int* p = &a;// int address = static_cast<int>(p); //errorreturn 0;
}

reinterpret_cast

用于两个不相关类型之间的转换。比如指针转整型

int main()
{int a = 10;int* p = &a;int address = reinterpret_cast<int>(p);cout << address << endl;return 0;
}

const_cast必须转指针

const_cast用于删除变量的const属性,转换后就可以对const变量的值进行修改。提供一种修改const常量的方法,C语言中可以间接修改const常量,C++类型检查更严格。比如:

int main()
{const int a = 2; //加valatile 下面变成int* p = const_cast<int*>(&a);*p = 3;cout << a << endl;  //2cout << *p << endl; //3return 0;
}

dynamic_cast转为子类指针

向上转型: 子类的指针(或引用)→ 父类的指针(或引用)。
向下转型: 父类的指针(或引用)→ 子类的指针(或引用)。

其中,向上转型就是所说的切割/切片,是语法天然支持的,不需要进行转换,而向下转型是语法不支持的,需要进行强制类型转换。

使用dynamic_cast进行向下转型则是安全的,如果父类的指针(或引用)指向的是子类对象那么dynamic_cast会转换成功,但如果父类的指针(或引用)指向的是父类对象那么dynamic_cast会转换失败并返回一个空指针。

class A
{
public:virtual void f(){}
};
class B : public A
{};
void func(A* pa)
{B* pb1 = (B*)pa;               //不安全B* pb2 = dynamic_cast<B*>(pa); //安全cout << "pb1: " << pb1 << endl;cout << "pb2: " << pb2 << endl;
}
int main()
{A a;B b;func(&a);func(&b);return 0;
}

说明一下: dynamic_cast只能用于含有虚函数的类,因为运行时类型检查需要运行时的类型信息,而这个信息是存储在虚函数表中的,只有定义了虚函数的类才有虚函数表。

explicit

explicit用来修饰单参数构造函数,从而禁止单参数构造函数的隐式转换。

RTTI(Run-Time Type Identification)就是运行时类型识别。

C++通过以下几种方式来支持RTTI:

  1. typeid:在运行时识别出一个对象的类型。
  2. dynamic_cast:在运行时识别出一个父类的指针(或引用)指向的是父类对象还是子类对象。
  3. decltype:在运行时推演出一个表达式或函数返回值的类型。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com