菜鸟码转
IP:
0关注数
23粉丝数
0获得的赞
工作年
编辑资料
链接我:

创作·169

全部
问答
动态
项目
学习
专栏
菜鸟码转

12.什么是类的默认构造函数

默认构造函数(default constructor)就是在没有显式提供初始化式时调用的构造函数。它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义。如果定义某个类的变量时没有提供初始化时就会使用默认构造函数。1.用户定义的默认构造函数:用户自定义的不带参数的构造函数:C++#include <iostream> using namespace std; class A { public: A(){ // 类的默认构造函数 var = 10; c = 'q'; } A(int val = 10) int var; char c; }; int main() { A ex; cout << ex.c << endl << ex.var << endl; return 0; } /* 运行结果: q 10 */说明:上述程序中定义变量 ex 时,未提供任何实参,程序运行时会调用默认的构造函数。用户自定义的构造函数,但为所有形参提供默认值的构造函数:C++#include <iostream> using namespace std; class A { public: A(int _var = 10, char _c = 'q'){ // 类的默认构造函数 var = _var; c = _c; } int var; char c; }; int main() { A ex; cout << ex.c << endl << ex.var << endl; return 0; } /* 运行结果: q 10 */说明:上述程序中定义变量 ex 时,未提供任何实参,程序运行时会调用所有形参提供默认值的构造函数。2.编译器自动分配的合成默认构造函数如果用户定义的类中没有显式的定义任何构造函数,编译器就会自动为该类型生成默认构造函数,称为合成的默认构造函数。C++#include <iostream> using namespace std; class A { public: int var; char c; }; int main() { A ex; cout << ex.c << endl << ex.var << endl; return 0; } /* 运行结果: 0 */此时编译器会自动为 A 分配一个默认的构造函数,在上述示例中,类 A 中的变量 c 默认赋值为 \0,var 默认赋值为 0。一般情况下,如果类中包含内置或复合类型的成员,则该类就不应该依赖于合成的默认构造函数,它应该定义自己的构造函数来初始化这些成员。多数情况下,编译器为类生成一个公有的默认构造函数,只有下面两种情况例外:一个类显式地声明了任何构造函数,编译器不生成公有的默认构造函数。在这种情况下,如果程序需要一个默认构造函数,需要由类的设计者提供。一个类声明了一个非公有的默认构造函数,编译器不会生成公有的默认构造函数。在大多数情况下,C++ 编译器为未声明构造函数之 class 合成一个默认构造函数:如果该类没有任何构造函数,但是包含一个对象类型的成员变量,且该变量有一个显式的默认构造函数;如果该类没有任何构造函数,但是其父类含有显式的默认构造函数;如果该类没有任何构造函数,但是含有(或父类含有)虚函数;如果该类没有任何构造函数,但是带有一个虚基类;
C++
0
0
0
浏览量2013
菜鸟码转

28.如何让类不能被继承

使用 final 关键字:使用 final 关键字修饰的类不能被继承。C++#include <iostream> using namespace std; class Base final { }; class Derive: public Base{ // error: cannot derive from 'final' base 'Base' in derived type 'Derive' }; int main() { Derive ex; return 0; }使用友元、虚继承和私有构造函数来实现C++#include <iostream> using namespace std; template <typename T> class Base{ friend T; private: Base(){ cout << "base" << endl; } ~Base(){} }; class B:virtual public Base<B>{ //一定注意 必须是虚继承 public: B(){ cout << "B" << endl; } }; class C:public B{ public: C(){} // error: 'Base<T>::Base() [with T = B]' is private within this context }; int main(){ B b; return 0; }说明:在上述代码中 B 类是不能被继承的类。具体原因:虽然 Base 类构造函数和析构函数被声明为私有 private,在 B 类中,由于 B 是 Base 的友元,因此可以访问 Base 类构造函数,从而正常创建 B 类的对象;B 类继承 Base 类采用虚继承的方式,创建 C 类的对象时,C 类的构造函数要负责 Base 类的构造,但是 Base 类的构造函数私有化了,C 类没有权限访问。因此,无法创建 C 类的对象, B 类是不能被继承的类。注意:在继承体系中,友元关系不能被继承,虽然 C 类继承了 B 类,B 类是 Base 类的友元,但是 C 类和 Base 类没有友元关系。
C++
0
0
0
浏览量2014
菜鸟码转

6.虚函数的实现机制

1.虚函数的实现原理:实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚函数表。该表是编译器在编译时设置的静态数组,一般我们称为 vtable。虚函数表包含可由该类调用的虚函数,此表中的每个条目是一个函数指针,指向该类可访问的虚函数。每个对象在创建时,编译器会为对象生成一个指向该类的虚函数表的指针,我们称之为 vptr。vptr 在创建类实例时自动设置,以便指向该类的虚拟表。如果对象(或者父类)中含有虚函数,则编译器一定会为其分配一个 vptr;如果对象不包含(父类也不含有),此时编译器则不会为其分配 vptr。与 this 指针不同,this 指针实际上是编译器用来解析自引用的函数参数,vptr 是一个真正的指针。虚函数表相关知识点:虚函数表存放的内容:类的虚函数的地址。虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针 vptr,来指向类的虚函数表 vtable。实例:无虚函数覆盖的情况:C++#include <iostream> using namespace std; class Base { public: virtual void B_fun1() { cout << "Base::B_fun1()" << endl; } virtual void B_fun2() { cout << "Base::B_fun2()" << endl; } virtual void B_fun3() { cout << "Base::B_fun3()" << endl; } }; class Derive : public Base { public: virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; } virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; } virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; } }; int main() { Base *p = new Derive(); p->B_fun1(); // Base::B_fun1() return 0; }基类和派生类的继承关系:基类的虚函数表:派生类的虚函数表:主函数中基类的指针 p 指向了派生类的对象,当调用函数 B_fun1() 时,通过派生类的虚函数表找到该函数的地址,从而完成调用。2.虚拟函数表指针 vptr:带有虚函数的类,通过该类所隐含的虚函数表来实现多态机制,该类的每个对象均具有一个指向本类虚函数表的指针,这一点并非 C++ 标准所要求的,而是编译器所采用的内部处理方式。实际应用场景下,不同平台、不同编译器厂商所生成的虚表指针在内存中的布局是不同的,有些将虚表指针置于对象内存中的开头处,有些则置于结尾处。如果涉及多重继承和虚继承,情况还将更加复杂。因此永远不要使用 C 语言的方式调用 memcpy() 之类的函数复制对象,而应该使用初始化(构造和拷构)或赋值的方式来复制对象。程序示例,我们通过对象内存的开头处取出 vptr,并遍历对象虚函数表。C++#include <iostream> #include <memory> using namespace std; typedef void (*func)(void); class A { public: void f() { cout << "A::f" << endl; } void g() { cout << "A::g" << endl; } void h() { cout << "A::h" << endl; } }; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; class Derive: public Base { public: void f() { cout << "Derive::f" << endl; } void g() { cout << "Derive::g" << endl; } void h() { cout << "Derive::h" << endl; } }; int main() { Base base; Derive derive; //获取vptr的地址,运行在gcc x64环境下,所以将指针按unsigned long *大小处理 //另外基于C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置 unsigned long* vPtr = (unsigned long*)(&base); //获取vTable 首个函数的地址 func vTable_f = (func)*(unsigned long*)(*vPtr); //获取vTable 第二个函数的地址 func vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算 func vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上 vTable_f(); vTable_g(); vTable_h(); vPtr = (unsigned long*)(&derive); //获取vTable 首个函数的地址 vTable_f = (func)*(unsigned long*)(*vPtr); //获取vTable 第二个函数的地址 vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算 vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上 vTable_f(); vTable_g(); vTable_h(); cout<<sizeof(A)<<endl; cout<<sizeof(base)<<endl; cout<<sizeof(derive)<<endl; return 0; } /* Base::f Base::g Base::h Derive::f Derive::g Derive::h 1 8 8 */我们可以看到同样的函数实现,对象在分配空间时,编译器会为对象多分配一个 vptr 指针的空间。3.虚函数的使用场景:构造函数不能为虚函数:构造函数不能定义为虚函数。构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。析构函数为虚函数:一般建议析构函数定义成虚函数,这样做可以有效是防止内存泄漏,实际应用时当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,当我们对基类指针执行 delete 操作时,此时只会调用基类的析构函数,将基类的成员所占的空间释放掉,而派生类中特有的资源就会无法释放而导致内存泄漏。static 函数不能定义为虚函数。
C++
0
0
0
浏览量2025
菜鸟码转

19.什么是可变参数模板

对于可变参数函数,在 C 语言中我们最熟悉的就是 printf 函数int printf(const char *format, ...)在 C++ 中的模板也可以支持可变参数:可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。模板参数包:表示零个或多个模板参数;函数参数包:表示零个或多个函数参数。用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof... 运算符。template <typename T, typename... Args> // Args 是模板参数包 void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包实例:#include <iostream> using namespace std; template <typename T> void print_fun(const T &t) { cout << t << endl; // 最后一个元素 } template <typename T, typename... Args> void print_fun(const T &t, const Args &...args) { cout << t << " "; print_fun(args...); } int main() { print_fun("Hello", "world", "!"); return 0; } /*运行结果: Hello wolrd ! */说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。
C++
0
0
0
浏览量2022
菜鸟码转

14.C++ 类对象的初始化顺序

1.构造函数调用顺序:按照派生类继承基类的顺序,即派生列表中声明的继承顺序,依次调用基类的构造函数;在有虚继承和一般继承存在的情况下,优先虚继承。比如虚继承:class C: public B, virtual public A,此时应当先调用 A 的构造函数,再调用 B 的构造函数。按照派生类中成员变量的声明顺序,依次调用派生类中成员变量所属类的构造函数;执行派生类自身的构造函数。2.类对象的初始化顺序:按照构造函数的调用顺序,调用基类的构造函数按照成员变量的声明顺序,调用成员变量的构造函数函数,成员变量的初始化顺序与声明顺序有关;调用该类自身的构造函数;析构顺序和类对象的初始化顺序相反。C++#include <iostream> using namespace std; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } }; class Test : public A, public B // 派生列表 { public: Test() { cout << "Test()" << endl; } ~Test() { cout << "~Test()" << endl; } private: B ex1; A ex2; }; int main() { Test ex; return 0; } /* 运行结果: A() B() B() A() Test() ~Test() ~A() ~B() ~B() ~A() */程序运行结果分析:首先调用基类 A 和 B 的构造函数,按照派生列表 public A, public B 的顺序构造;然后调用派生类 Test 的成员变量 ex1 和 ex2 的构造函数,按照派生类中成员变量声明的顺序构造;最后调用派生类的构造函数;接下来调用析构函数,和构造函数调用的顺序相反。3.类的成员初始化:类中可能含有静态变量和全局变量,由于静态变量和全局变量都被放在静态存储区,他们的初始化在 main 函数执行之前已被初始化,且 static 变量必须在类外进行初始化。成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。如果类不使用初始化列表初始化,而在类的构造函数内部进行初始化时,此时成员变量的初始化顺序与构造函数中代码逻辑有关。类成员在定义时,是不能初始化的类中 const 成员常量必须在构造函数初始化列表中初始化。类中 static 成员变量,必须在类外初始化。
C++
0
0
0
浏览量2015
菜鸟码转

15.函数模板和类模板的区别

实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。默认参数:函数模板不允许有默认参数,类模板在模板参数列表中可以有默认参数。特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。调用方式不同:函数模板可以进行类型推导,可以隐式调用,也可以显式调用;类模板只能显式调用。函数模板调用方式举例:#include<iostream> using namespace std; template <typename T> T add_fun(const T & tmp1, const T & tmp2){ return tmp1 + tmp2; } int main(){ int var1, var2; cin >> var1 >> var2; cout << add_fun<int>(var1, var2); // 显式调用 double var3, var4; cin >> var3 >> var4; cout << add_fun(var3, var4); // 隐式调用 return 0; }
C++
0
0
0
浏览量2017
菜鸟码转

2.线程同步与异步

线程同步操作:C++ 标准库提供了以下几种线程同步的方式:互斥量(支持超时加锁、递归加锁);读写锁(共享互斥量,也支持超时加锁);互斥量包装器(基于 RAII 的思想);条件变量;信号量(二元信号量、计数信号量);barrier;call_once;不同的同步方式具有不同的使用场景和性能,实际使用时根据不同的场景选择不同的同步方式,分别就几种以上几种方式进行简要介绍:1.互斥量(mutex):互斥量(mutex)是防止同时访问共享资源的程序对象。为避免线程更新共享变量时所出现问题,必须使用互斥量(mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。 即使用互斥量来实现原子访问操作,防止多个线程对临界区同时操作而产生不一致的问题。mutex 只有锁定(locked)和未锁定(unlocked)两种状态。任何时候,至多只有一个线程可以锁定互斥量。试图对已经锁定的互斥量再次加锁,将可能阻塞线程或者报错失败,mutex 的底层可能封装的是操作系统 spinlock,不同的操作系统下可能有不同的实现。C++ 中关于 mutex 的头文件为 #include <mutex>。C++#include <iostream> #include <thread> #include <mutex> std::mutex mtx; void print_block (int n, char c) { mtx.lock(); for (int i=0; i<n; ++i) { std::cout << c; } std::cout << '\n'; mtx.unlock(); } int main () { std::thread th1 (print_block,50,'*'); std::thread th2 (print_block,50,'$'); th1.join(); th2.join(); return 0; } /* **************************************** $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ */C++ 中还定义了 timed_mutex:在 mutex 的基础上增加了超时加锁的功能。recursive_mutex:在 mutex 的基础上增加了递归加锁的功能(此时,lock() 函数可以被同一线程在不释放锁的情况下多次调用)。C++std::recursive_mutex mtx; void fun1() { mtx.lock(); mtx.unlock(); } void fun2() { mtx.lock(); fun1(); // recursive lock mtx.unlock(); };2.共享互斥量:std::shared_mutex 是 C++ 17 标准中引入的,由 unique_lock 和 shared_lock 两个类模板配合 shared_mutex 使用,主要用于读写共享锁。unique_lock 用于写入时加锁,shared_lock 用于读取时加锁。对象在构造时自动对 std::shared_mutex 加锁,析构时自动对其解锁。头文件主要包含在 #include <shared_mutex>。shared_mutex 可用于保护共享数据不被多个线程同时访问。与其他便于独占访问的互斥锁类型相比,shared_mutex 具有两个访问级别:shared:多个线程可以共享同一个互斥锁的所有权。exclusive :只有一个线程可以拥有互斥锁。共享互斥锁通常用于多个读取操作可以同时访问同一资源而不会导致数据竞争,但只有一个写入操作的场景。C++#include <iostream> #include <mutex> // For std::unique_lock #include <shared_mutex> #include <thread> class ThreadSafeCounter { public: ThreadSafeCounter() = default; // 多个线程可以同时读取 countter 计数 unsigned int get() const { std::shared_lock lock(mutex_); return value_; } // 只有1个线程可以修改 countter 计数 void increment() { std::unique_lock lock(mutex_); value_++; } // 只有1个线程可以修改 countter 计数 void reset() { std::unique_lock lock(mutex_); value_ = 0; } private: mutable std::shared_mutex mutex_; unsigned int value_ = 0; }; int main() { ThreadSafeCounter counter; auto increment_and_print = [&counter]() { for (int i = 0; i < 3; i++) { counter.increment(); std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n'; // Note: Writing to std::cout actually needs to be synchronized as well // by another std::mutex. This has been omitted to keep the example small. } }; std::thread thread1(increment_and_print); std::thread thread2(increment_and_print); thread1.join(); thread2.join(); } /* 139677317637888 2 139677317637888 3 139677309245184 4 139677309245184 5 139677309245184 6 139677317637888 6 */我们可以看到 increment 同时只能有一个线程对计数进行增加,但可能同时存在多个线程读取同一个计数。shared_timed_mutex 是在 shared_mutex 的基础上增加了超时加锁的功能。3.互斥量包装器lock_guard:使用了 RAII 的机制对互斥量进行类模板封装,构造时加锁,析构时解锁。C++#include <mutex> std::mutex mtx; void f() { const std::lock_guard<std::mutex> lock(mtx); // ... // mtx is automatically released when lock goes out of scope }互斥量包装器对比原生的 mutex 来说,创建即加锁,作用域结束自动析构并解锁,无需手动解锁。缺点是不能中途解锁,不支持复制和移动。在需要加锁的地方,只需要任意实例化一个 lock_guard,调用构造函数成功上锁,出作用域时则 lock_guard 对象会被销毁,调用析构函数自动解锁可以有效避免死锁问题,但是提供的功能单一且不够灵活。unique_lock:unique_lock 类模板也是采用 RAII 的方式对锁进行了封装,并且也是以独占所有权的方式管理 mutex 对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动 move 赋值)时,unique_lock 对象需要传递一个 mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化 unique_lock 的对象时,自动调用构造函数上锁,unique_lock 对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。与 lock_guard 不同的是,unique_lock 更加的灵活,提供了更多的成员函数:上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until 和 unlock;修改操作:支持移动赋值、交换(swap:与另一个 unique_lock 对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)。获取属性:owns_lock (返回当前对象是否上了锁)、operator bool() (与 owns_lock() 的功能相同)、mutex(返回当前 unique_lock 所管理的互斥量的指针)。4.条件变量(condition variable):在 C++ 11 以后,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。C++ 中包含的头文件在 #include <condition_variable> 中。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程因等待 条件变量的条件成立 而挂起;另外一个线程使 条件成立 从而给出唤醒线程的信号,从而唤醒被等待的线程;为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起;通常情况下这个锁是 std::mutex,并且管理这个锁只能是 std::unique_lock std::mutex 等 RAII 模板类。分别是使用以下两个方法实现:等待条件成立使用的是 condition_variable 类成员 wait 、wait_for 或 wait_until。唤醒信号使用的是 condition_variable 类成员 notify_one 或者 notify_all 函数。我们可以看到 wait 函数如下:C++template< class Predicate > void wait( std::unique_lock<std::mutex>& lock, Predicate stop_waiting );线程会一直挂起,直到 stop_waiting 为 true 为止。程序示例如下:C++#include <iostream> #include <string> #include <thread> #include <mutex> #include <condition_variable> std::mutex m; std::condition_variable cv; std::string data; bool ready = false; bool processed = false; void worker_thread() { std::unique_lock<std::mutex> lk(m); // worker 线程等待 ready cv.wait(lk, []{return ready;}); // 唤醒执行 std::cout << "Worker thread is processing data\n"; data += " after processing"; // processed 设置为 true, 唤醒 main 线程 processed = true; std::cout << "Worker thread signals data processing completed\n"; // 释放锁,防止再次被唤醒。 lk.unlock(); // 唤醒 main 线程 cv.notify_one(); } int main() { std::thread worker(worker_thread); // 让 worker 线程先执行,再进行唤醒,否则可能出现 ready = true 先于 worker 线程的执行 worker.detach(); data = "Example data"; // 设置 ready 为 true, 唤醒 worker 线程 { std::lock_guard<std::mutex> lk(m); ready = true; std::cout << "main() signals data ready for processing\n"; } // 唤醒 worker 线程 cv.notify_one(); // 等待 worker 线程 { std::unique_lock<std::mutex> lk(m); cv.wait(lk, []{return processed;}); } std::cout << "Back in main(), data = " << data << '\n'; return 0; }5.信号量:C++ 20 中添加了 C++ 中的信号量为二元信号量与计数信号量,二元信号量实际为计数信号量模板的特化。binary_semaphore:二元信号量类似于互斥量,信号量只有 0 与 1 。counting_semaphore:计数信号量所有关于信号量的定义参考头文件 #include <semaphore>,计数信号量是一种轻量级同步原语,可以控制对共享资源的访问。与 std::mutex 不同的是,acounting_semaphore 至少允许 LeastMaxValue 并发访问者对同一资源进行多个并发访问。Acounting_semaphore 包含一个由构造函数初始化的内部计数器。该计数器可以通过 acquire() 获取资源访问权限,并通过调用 release() 来释放资源从而递增计数器。当计数器为零时,调用 acquire() 时就会阻塞直到计数器增加,但是调用 try_acquire( ) 不阻塞;try_acquire_for() 和 try_acquire_until() 阻塞直到计数器增加或达到超时。C++#include <iostream> #include <thread> #include <chrono> #include <semaphore> std::binary_semaphore smphSignalMainToThread{0}, smphSignalThreadToMain{0}; void ThreadProc() { // 第一次进入阻塞 smphSignalMainToThread.acquire(); std::cout << "[thread] Got the signal\n"; // response message using namespace std::literals; std::this_thread::sleep_for(3s); std::cout << "[thread] Send the signal\n"; // message // 唤醒 main 线程 smphSignalThreadToMain.release(); } int main() { std::thread thrWorker(ThreadProc); std::cout << "[main] Send the signal\n"; // message // 唤醒 ThreadProc smphSignalMainToThread.release(); // main 线程阻塞 smphSignalThreadToMain.acquire(); std::cout << "[main] Got the signal\n"; // response message thrWorker.join(); } /* [main] Send the signal [thread] Got the signal [thread] Send the signal [main] Got the signal */6.barrier:C++ 20 以后支持 latch 与 barrier,他们同样可以用来线程同步。latch:类 latch 是 std::ptrdiff_t 类型的向下计数器,可用于同步线程。计数器的值在创建时初始化。线程可能会阻塞在锁存器上,直到计数器减为零。不能增加或重置计数器,这使得锁存器创建后不可重用。其内部维护着一个计数器,当计数不为 0 时,所有参与者(线程)都将阻塞在等待操作处,计数为 0 时,解除阻塞。计数器不可重置或增加,故它是一次性的,不可重用。与 std::barrier 不同,std::latch 参与线程可以多次递减。C++#include <latch> std::latch work_done(4); work_done.count_down(); // decrements the counter in a non-blocking manner work_done.wait(); // blocks until the counter reaches zero bool ok = work_done.try_wait(); // tests if the internal counter equals zero work_done.arrive_and_wait(); // decrements the counter and blocks until it reaches zerobarrier:类似于 latch,它会阻塞线程直到所有参与者线程都到达一个同步点,直到预期数量的线程到达设定的值则会接触阻塞。与 latch 不同的是,它是可重用的。一个 barrier 的生命周期包含多个阶段,每个阶段都定义了一个同步点。一个 barrier 阶段包含:期望计数(设创建时指定的计数为 n),当期望计数不为 0 时,参与者将阻塞于等待操作处;当期望计数为 0 时,会执行创建 barrier 时指定的阶段完成步骤,然后解除阻塞所有阻塞于同步点的参与者线程。当阶段完成步骤执行完成后,会重置期望计数为 n - 调用arrive_and_drop() 的次数,然后开始下一个阶段。C++#include <barrier> #include <iostream> #include <string> #include <thread> #include <vector> int main() { const auto workers = { "anil", "busara", "carl" }; auto on_completion = []() noexcept { // locking not needed here static auto phase = "... done\n" "Cleaning up...\n"; std::cout << phase; phase = "... done\n"; }; std::barrier sync_point(std::ssize(workers), on_completion); auto work = [&](std::string name) { std::string product = " " + name + " worked\n"; std::cout << product; // ok, op<< call is atomic sync_point.arrive_and_wait(); // 全部到达后,进行下一阶段 product = " " + name + " cleaned\n"; std::cout << product; sync_point.arrive_and_wait(); }; std::cout << "Starting...\n"; std::vector<std::thread> threads; for (auto const& worker : workers) { threads.emplace_back(work, worker); } for (auto& thread : threads) { thread.join(); } } /* Starting... anil worked carl worked busara worked ... done Cleaning up... busara cleaned anil cleaned carl cleaned ... done */7.call_once:C++ 11 以后支持 call_once。确保某个操作只被执行一次(成功执行才算),即使是多线程环境下也确保只执行一次。C++template< class Callable, class... Args > void call_once( std::once_flag& flag, Callable&& f, Args&&... args );如果在 call_once 被调用时,flag 表明 f 已经被调用,则 call_once 立即返回(这种调用 call_once 称为被动)。C++#include <iostream> #include <thread> #include <mutex> std::once_flag flag1, flag2; void simple_do_once() { std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; }); } int main() { std::thread st1(simple_do_once); std::thread st2(simple_do_once); std::thread st3(simple_do_once); std::thread st4(simple_do_once); st1.join(); st2.join(); st3.join(); st4.join(); } /* Simple example: called once */
C++
0
0
0
浏览量2045
菜鸟码转

2.工厂模式及其实现

工厂模式:包括简单工厂模式、抽象工厂模式、工厂方法模式。简单工厂模式:主要用于创建对象。用一个工厂来根据输入的条件产生不同的类,然后根据不同类的虚函数得到不同的结果。工厂方法模式:修正了简单工厂模式中不遵守开放封闭原则。把选择判断移到了客户端去实现,如果想添加新功能就不用修改原来的类,直接修改客户端即可。抽象工厂模式:定义了一个创建一系列相关或相互依赖的接口,而无需指定他们的具体类。1.简单工厂模式主要用于创建对象。用一个工厂来根据输入的条件产生不同的类,然后根据不同类的虚函数得到不同的结果。应用场景:适用于针对不同情况创建不同类时,只需传入工厂类的参数即可,无需了解具体实现方法。例如:计算器中对于同样的输入,执行不同的操作:加、减、乘、除。实现方式:C++#include <iostream> #include <vector> using namespace std; // Here is the product class class Operation { public: int var1, var2; virtual double GetResult() { double res = 0; return res; } }; class Add_Operation : public Operation { public: virtual double GetResult() { return var1 + var2; } }; class Sub_Operation : public Operation { public: virtual double GetResult() { return var1 - var2; } }; class Mul_Operation : public Operation { public: virtual double GetResult() { return var1 * var2; } }; class Div_Operation : public Operation { public: virtual double GetResult() { return var1 / var2; } }; // Here is the Factory class class Factory { public: static Operation *CreateProduct(char op) { switch (op) { case '+': return new Add_Operation(); case '-': return new Sub_Operation(); case '*': return new Mul_Operation(); case '/': return new Div_Operation(); default: return new Add_Operation(); } } }; int main() { int a, b; cin >> a >> b; Operation *p = Factory::CreateProduct('+'); p->var1 = a; p->var2 = b; cout << p->GetResult() << endl; p = Factory::CreateProduct('*'); p->var1 = a; p->var2 = b; cout << p->GetResult() << endl; return 0; }2.工厂方法模式修正了简单工厂模式中不遵守开放封闭原则。把选择判断移到了客户端去实现,如果想添加新功能就不用修改原来的类,直接修改客户端即可。应用场景:一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。一个类通过其派生类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其派生类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,派生类对象将覆盖父类对象,从而使得系统更容易扩展。将创建对象的任务委托给多个工厂派生类中的某一个,客户端在使用时可以无须关心是哪一个工厂派生类创建产品派生类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。实现方式:C++#include <iostream> #include <vector> using namespace std; // Here is the product class class Operation { public: int var1, var2; virtual double GetResult() { double res = 0; return res; } }; class Add_Operation : public Operation { public: virtual double GetResult() { return var1 + var2; } }; class Sub_Operation : public Operation { public: virtual double GetResult() { return var1 - var2; } }; class Mul_Operation : public Operation { public: virtual double GetResult() { return var1 * var2; } }; class Div_Operation : public Operation { public: virtual double GetResult() { return var1 / var2; } }; class Factory { public: virtual Operation *CreateProduct() = 0; }; class Add_Factory : public Factory { public: Operation *CreateProduct() { return new Add_Operation(); } }; class Sub_Factory : public Factory { public: Operation *CreateProduct() { return new Sub_Operation(); } }; class Mul_Factory : public Factory { public: Operation *CreateProduct() { return new Mul_Operation(); } }; class Div_Factory : public Factory { public: Operation *CreateProduct() { return new Div_Operation(); } }; int main() { int a, b; cin >> a >> b; Add_Factory *p_fac = new Add_Factory(); Operation *p_pro = p_fac->CreateProduct(); p_pro->var1 = a; p_pro->var2 = b; cout << p_pro->GetResult() << endl; Mul_Factory *p_fac1 = new Mul_Factory(); Operation *p_pro1 = p_fac1->CreateProduct(); p_pro1->var1 = a; p_pro1->var2 = b; cout << p_pro1->GetResult() << endl; return 0; }3.抽象工厂模式定义了一个创建一系列相关或相互依赖的接口,而无需指定他们的具体类。应用场景:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。系统中有多于一个的产品族,而每次只使用其中某一产品族。属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。实现方式:C++#include <iostream> #include <vector> using namespace std; // Here is the product class class Operation_Pos { public: int var1, var2; virtual double GetResult() { double res = 0; return res; } }; class Add_Operation_Pos : public Operation_Pos { public: virtual double GetResult() { return var1 + var2; } }; class Sub_Operation_Pos : public Operation_Pos { public: virtual double GetResult() { return var1 - var2; } }; class Mul_Operation_Pos : public Operation_Pos { public: virtual double GetResult() { return var1 * var2; } }; class Div_Operation_Pos : public Operation_Pos { public: virtual double GetResult() { return var1 / var2; } }; /*********************************************************************************/ class Operation_Neg { public: int var1, var2; virtual double GetResult() { double res = 0; return res; } }; class Add_Operation_Neg : public Operation_Neg { public: virtual double GetResult() { return -(var1 + var2); } }; class Sub_Operation_Neg : public Operation_Neg { public: virtual double GetResult() { return -(var1 - var2); } }; class Mul_Operation_Neg : public Operation_Neg { public: virtual double GetResult() { return -(var1 * var2); } }; class Div_Operation_Neg : public Operation_Neg { public: virtual double GetResult() { return -(var1 / var2); } }; /*****************************************************************************************************/ // Here is the Factory class class Factory { public: virtual Operation_Pos *CreateProduct_Pos() = 0; virtual Operation_Neg *CreateProduct_Neg() = 0; }; class Add_Factory : public Factory { public: Operation_Pos *CreateProduct_Pos() { return new Add_Operation_Pos(); } Operation_Neg *CreateProduct_Neg() { return new Add_Operation_Neg(); } }; class Sub_Factory : public Factory { public: Operation_Pos *CreateProduct_Pos() { return new Sub_Operation_Pos(); } Operation_Neg *CreateProduct_Neg() { return new Sub_Operation_Neg(); } }; class Mul_Factory : public Factory { public: Operation_Pos *CreateProduct_Pos() { return new Mul_Operation_Pos(); } Operation_Neg *CreateProduct_Neg() { return new Mul_Operation_Neg(); } }; class Div_Factory : public Factory { public: Operation_Pos *CreateProduct_Pos() { return new Div_Operation_Pos(); } Operation_Neg *CreateProduct_Neg() { return new Div_Operation_Neg(); } }; int main() { int a, b; cin >> a >> b; Add_Factory *p_fac = new Add_Factory(); Operation_Pos *p_pro = p_fac->CreateProduct_Pos(); p_pro->var1 = a; p_pro->var2 = b; cout << p_pro->GetResult() << endl; Add_Factory *p_fac1 = new Add_Factory(); Operation_Neg *p_pro1 = p_fac1->CreateProduct_Neg(); p_pro1->var1 = a; p_pro1->var2 = b; cout << p_pro1->GetResult() << endl; return 0; }
C++
0
0
0
浏览量2019
菜鸟码转

18.编译时多态和运行时多态的区别

编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。实际在编译器内部看来不管是重载还是模板,编译器内部都会生成不同的函数,在代码段中分别装有两个函数的不同实现。运行时多态:运行时多态也称动态绑定,在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。2.编译时多态和运行时多态的区别:时期不同:编译时多态发生在程序编译过程中,运行时多态发生在程序的运行过程中;实现方式不同:编译时多态运用泛型编程来实现,运行时多态借助虚函数表来实现。
C++
0
0
0
浏览量2015
菜鸟码转

17.静态绑定和动态绑定的实现

静态类型和动态类型:静态类型:变量在声明时的类型,是在编译阶段确定的。静态类型不能更改。动态类型:目前所指对象的类型,是在运行阶段确定的。动态类型可以更改。静态绑定和动态绑定:静态绑定是指程序在编译阶段确定对象的类型(静态类型)。动态绑定是指程序在运行阶段确定对象的类型(动态类型)。静态绑定和动态绑定的区别:发生的时期不同:如上。对象的静态类型不能更改,动态类型可以更改。注:对于类的成员函数,只有虚函数是动态绑定,其他都是静态绑定。C++#include <iostream> using namespace std; class Base { public: virtual void fun() { cout << "Base::fun()" << endl; } }; class Derive : public Base { public: void fun() { cout << "Derive::fun()"; } }; int main() { Base *p = new Derive(); // p 的静态类型是 Base*,动态类型是 Derive* p->fun(); // fun 是虚函数,运行阶段进行动态绑定 return 0; } /* 运行结果: Derive::fun() */动态绑定的实现原理:以下程序示例:C++#include <iostream> using namespace std; class A{ private: int a1; int a2; public: virtual void display(){ cout<<"A::display()"<<endl;} virtual void clone(){ cout<<"A::clone()"<<endl;} }; class B: public A{ private: int b; public: virtual void display(){ cout<<"B::display()"<<endl;} override virtual void init(){ cout<<"B::init()"<<endl;} }; class C: public B{ private: int c; public: virtual void display(){ cout<<"C::display()"<<endl;} override virtual void execute(){ cout<<"C::execute()"<<endl;} virtual void init(){cout<<"C::init()"<<endl;} override }; int main() { A *p1 = new B(); A *p2 = new C(); p1->display(); p2->display(); return 0; }我们对上述程序进行编译,并查看这里给出 A, B, C 三个类的虚函数表,如下图所示:可以得出以下结论:类的内存占用由成员变量和指向虚函数表的指针组成,同时派生类的成员变量是会把基类的成员变量都继承的同名虚函数在基类和派生类中的虚函数表中,在虚函数表中偏移位置是一致的,图 A,B,C 的 display 的偏移位置都为 0。同样名称的虚函数,在基类中定义的虚函数与派生类中定义的虚函数,在虚函数表中的偏移量都是一致的,只有这样才能保证动态绑定。如果派生类中定义了与基类同名的虚函数,那么派生类的虚函数表中响应函数的入口地址会被替换成覆盖后的函数的地址。一旦有新的虚函数定义,会加入到当前虚函数表的末端。派生类的成员变量顺序也按照声明的顺序依次在内存中分配。我们可以分以一下动态绑定的实现:当我们用虚函数表指针去查找虚函数表中对应的函数的地址时,此时首先会找到函数地址的在虚函数表中的索引,这里 display 索引是 0。然后编译器会做一个替换,(*(p->vptr)[0]),找到 p 指针的函数入口地址。程序运行后会执行这条语句 *(p->vptr)[0](),完成函数的调用,实际即完成了动态绑定。
C++
0
0
0
浏览量380
菜鸟码转

7.构造函数、析构函数是否可以定义成虚函数

1.构造函数一般不定义为虚函数:从存储空间的角度考虑:构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。从使用的角度考虑:虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的。从实现上考虑:虚函数表指针是在创建对象之后才有的,因此不能定义成虚函数。从类型上考虑:在创建对象时需要明确其类型。2.析构函数一般定义成虚函数:析构函数定义成虚函数是为了防止内存泄漏,因为当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。比如以下程序示例:C++#include <iostream> using namespace std; class A { private: int val; public: ~A() { cout<<"A destroy!"<<endl; } }; class B: public A { private: int *arr; public: B() { arr = new int[10]; } ~B() { cout<<"B destroy!"<<endl; delete arr; } }; int main() { A *base = new B(); delete base; return 0; } // A destroy!我们可以看到如果析构函数不定义为虚函数,此时执行析构的只有基类,而派生类没有完成析构。我们将析构函数定义为虚函数,在执行析构时,则根据对象的类型来执行析构函数,此时派生类的资源得到释放。C++#include <iostream> using namespace std; class A { private: int val; public: virtual ~A() { cout<<"A destroy!"<<endl; } }; class B: public A { private: int *arr; public: B() { arr = new int[10]; } virtual ~B() { cout<<"B destroy!"<<endl; delete arr; } }; int main() { A *base = new B(); delete base; return 0; }
C++
0
0
0
浏览量2019
菜鸟码转

10.单继承和多继承的虚函数表结构

编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。如果派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。如果派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。如果派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。1.单继承无虚函数覆盖的情况:C++#include <iostream> #include <memory> using namespace std; typedef void (*func)(void); #include <iostream> using namespace std; class Base { public: virtual void B_fun1() { cout << "Base::B_fun1()" << endl; } virtual void B_fun2() { cout << "Base::B_fun2()" << endl; } virtual void B_fun3() { cout << "Base::B_fun3()" << endl; } }; class Derive : public Base { public: virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; } virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; } virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; } }; void printVtable(unsigned long *vptr, int offset) { func fn = (func)*((unsigned long*)(*vptr) + offset); fn(); } int main() { Base *p = new Derive(); p->B_fun1(); // Base::B_fun1() unsigned long* vPtr = (unsigned long*)(p); printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); printVtable(vPtr, 3); printVtable(vPtr, 3); printVtable(vPtr, 4); cout<<sizeof(Base)<<endl; // 8 cout<<sizeof(Derive)<<endl; // 8 return 0; } /* Base::B_fun1() Base::B_fun1() Base::B_fun2() Base::B_fun3() Derive::D_fun1() Derive::D_fun2() Derive::D_fun3() 8 8 */基类和派生类的继承关系:基类的虚函数表:派生类的虚函数表:虚函数按照声明顺序放在虚函数表里面。父类的虚函数在子类的虚函数前面。2.单继承有虚函数覆盖的情况:C++#include <iostream> #include <memory> using namespace std; typedef void (*func)(void); class Base { public: virtual void fun1() { cout << "Base::fun1()" << endl; } virtual void B_fun2() { cout << "Base::B_fun2()" << endl; } virtual void B_fun3() { cout << "Base::B_fun3()" << endl; } }; class Derive : public Base { public: virtual void fun1() { cout << "Derive::fun1()" << endl; } virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; } virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; } }; void printVtable(unsigned long *vptr, int offset) { func fn = (func)*((unsigned long*)(*vptr) + offset); fn(); } int main() { Base *p = new Derive(); unsigned long* vPtr = (unsigned long*)(p); printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); printVtable(vPtr, 3); printVtable(vPtr, 4); cout<<sizeof(Base)<<endl; // 8 cout<<sizeof(Derive)<<endl; // 8 return 0; } /* Derive::fun1() Base::B_fun2() Base::B_fun3() Derive::D_fun2() Derive::D_fun3() 8 8 */派生类的虚函数表:3.多继承无虚函数覆盖的情况:C++#include <iostream> using namespace std; class Base1 { public: virtual void B1_fun1() { cout << "Base1::B1_fun1()" << endl; } virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; } virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; } }; class Base2 { public: virtual void B2_fun1() { cout << "Base2::B2_fun1()" << endl; } virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; } virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; } }; class Base3 { public: virtual void B3_fun1() { cout << "Base3::B3_fun1()" << endl; } virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; } virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; } }; class Derive : public Base1, public Base2, public Base3 { public: virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; } virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; } virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; } }; typedef void (*func)(void); void printVtable(unsigned long *vptr, int offset) { func fn = (func)*((unsigned long*)(*vptr) + offset); fn(); } int main(){ Base1 *p = new Derive(); unsigned long* vPtr = (unsigned long*)(p); printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); printVtable(vPtr, 3); printVtable(vPtr, 4); printVtable(vPtr, 5); vPtr++; printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); vPtr++; printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); cout<<sizeof(Base1)<<endl; // 8 cout<<sizeof(Base2)<<endl; // 8 cout<<sizeof(Base3)<<endl; // 8 cout<<sizeof(Derive)<<endl; // 8 return 0; } /* Base1::B1_fun1() Base1::B1_fun2() Base1::B1_fun3() Derive::D_fun1() Derive::D_fun2() Derive::D_fun3() Base2::B2_fun1() Base2::B2_fun2() Base2::B2_fun3() Base3::B3_fun1() Base3::B3_fun2() Base3::B3_fun3() 8 8 8 24 */派生类的虚函数表:(基类的顺序和声明的顺序一致)4.多继承有虚函数覆盖的情况:C++#include <iostream> using namespace std; class Base1 { public: virtual void fun1() { cout << "Base1::fun1()" << endl; } virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; } virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; } }; class Base2 { public: virtual void fun1() { cout << "Base2::fun1()" << endl; } virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; } virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; } }; class Base3 { public: virtual void fun1() { cout << "Base3::fun1()" << endl; } virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; } virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; } }; class Derive : public Base1, public Base2, public Base3 { public: virtual void fun1() { cout << "Derive::fun1()" << endl; } virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; } virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; } }; typedef void (*func)(void); void printVtable(unsigned long *vptr, int offset) { func fn = (func)*((unsigned long*)(*vptr) + offset); fn(); } int main(){ Base1 *p1 = new Derive(); Base2 *p2 = new Derive(); Base3 *p3 = new Derive(); p1->fun1(); // Derive::fun1() p2->fun1(); // Derive::fun1() p3->fun1(); // Derive::fun1() unsigned long* vPtr = (unsigned long*)(p1); printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); printVtable(vPtr, 3); printVtable(vPtr, 4); vPtr++; printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); vPtr++; printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); cout<<sizeof(Base1)<<endl; // 8 cout<<sizeof(Base2)<<endl; // 8 cout<<sizeof(Base3)<<endl; // 8 cout<<sizeof(Derive)<<endl; // 8 return 0; } /* Derive::fun1() Derive::fun1() Derive::fun1() Derive::fun1() Base1::B1_fun2() Base1::B1_fun3() Derive::D_fun2() Derive::D_fun3() Derive::fun1() Base2::B2_fun2() Base2::B2_fun3() Derive::fun1() Base3::B3_fun2() Base3::B3_fun3() 8 8 8 24 */基类和派生类的关系:派生类的虚函数表:
C++
0
0
0
浏览量945
菜鸟码转

13.结构体相等的判断方式及 memcmp 函数的使用

1.符号重载:需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。利用运算符重载来实现结构体对象的比较:#include <iostream> using namespace std; struct A { char c; int val; A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {} friend bool operator==(const A &tmp1, const A &tmp2); // 友元运算符重载函数 }; bool operator==(const A &tmp1, const A &tmp2) { return (tmp1.c == tmp2.c && tmp1.val == tmp2.val); } int main() { A ex1('a', 90), ex2('b', 80); if (ex1 == ex2) cout << "ex1 == ex2" << endl; else cout << "ex1 != ex2" << endl; // 输出 return 0; }
C++
0
0
0
浏览量1636
菜鸟码转

5.虚函数和纯虚函数的区别

虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类(含有纯虚函数的类称为抽象基类)。使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上 virtual 关键字还需要加上 =0;虚函数必须实现,否则编译器会报错;对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。
C++
0
0
0
浏览量2014
菜鸟码转

11.如何禁止构造函数的使用

为类的构造函数增加 = delete 修饰符,可以达到虽然声明了构造函数但禁止使用的目的。C++#include <iostream> using namespace std; class A { public: int var1, var2; A(){ var1 = 10; var2 = 20; } A(int tmp1, int tmp2) = delete; }; int main() { A ex1; A ex2(12,13); // error: use of deleted function 'A::A(int, int)' return 0; }如果我们仅仅将构造函数设置为私有,类内部的成员和友元还可以访问,无法完全禁止。而在 C++11 以后,在成员函数声明后加 "= delete"则可以禁止该函数的使用,而需要保留的加 "= default"。
C++
0
0
0
浏览量2019
菜鸟码转

3.指针及其大小、用法

1.指针的定义:指针是一种变量类型,其值为另一个变量的地址,即内存位置的直接地址。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。在 64 位计算机中,指针占 8 个字节空间。使用指针时可以用以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。C++#include<iostream> using namespace std; int main(){ int *p = nullptr; cout << sizeof(p) << endl; // 8 char *p1 = nullptr; cout << sizeof(p1) << endl; // 8 return 0; }2.指针的用法:空指针:C 语言中定义了空指针为 NULL,实际是一个宏,它的值是 0,即 #define NULL 0。C++ 中使用 nullptr 表示空指针,它是 C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。指针的运算:两个同类型指针可以比较大小;两个同类型指针可以相减;指针变量可以和整数类型变量或常量相加;指针变量可以减去一个整数类型变量或常量;指针变量可以自增,自减;C++int a[10]; int *p1 = a + 1; // 指针常量相加 int *p2 = a + 4; bool greater = p2 > p1; // 比较大小 int offset = p2 - a; // 相减 p2++; // 自增 p1--; // 自减指向普通对象的指针:C++#include <iostream> using namespace std; class A { }; int main() { A *p = new A(); return 0; }指向常量对象的指针:常量指针,const 修饰表示指针指向的内容不能更改。C++#include <iostream> using namespace std; int main(void) { const int c_var = 10; const int * p = &c_var; cout << *p << endl; return 0; }指向函数的指针:函数指针。C++#include <iostream> using namespace std; int add(int a, int b){ return a + b; } typedef int (*fun_p)(int, int); int main(void) { fun_p fn = add; cout << fn(1, 6) << endl; return 0; }指向对象成员的指针,包括指向对象成员函数的指针和指向对象成员变量的指针。特别注意:定义指向成员函数的指针时,要标明指针所属的类。C++#include <iostream> using namespace std; class A { public: int var1, var2; static int x; static int get() { return 100; } int add(){ return var1 + var2; } }; int main() { A ex; ex.var1 = 3; ex.var2 = 4; int *p = &ex.var1; // 指向对象成员变量的指针 cout << *p << endl; int (A::*fun_p)(); int (*fun_q)(); fun_p = &A::add; // 指向对象非静态成员函数的指针 fun_p fun_q = A::get; // 指向对象静态成员函数的指针 fun_q cout << (ex.*fun_p)() << endl; cout << (*fun_q)() << endl; return 0; }而对于函数类型到函数指针类型的默认转换,只有当函数类型是左值的时候才行。所有对于非静态的成员函数,就不存在这种从函数类型到函数指针类型的默认转换,于是编译器也就不知道这个 p = A::add 该怎么确定。由于非静态成员函数指针可以有多态行为,在编译期函数地址可能无法确定。静态成员函数指针在编译期函数地址则可以确定。this 指针:指向类的当前对象的指针常量。C++#include <iostream> #include <cstring> using namespace std; class A { public: void set_name(string tmp) { this->name = tmp; } void set_age(int tmp) { this->age = age; } void set_sex(int tmp) { this->sex = tmp; } void show() { cout << "Name: " << this->name << endl; cout << "Age: " << this->age << endl; cout << "Sex: " << this->sex << endl; } private: string name; int age; int sex; };
C++
0
0
0
浏览量2016
菜鸟码转

3.观察者模式及其实现

观察者模式:定义一种一(被观察类)对多(观察类)的关系,让多个观察对象同时监听一个被观察对象,被观察对象状态发生变化时,会通知所有的观察对象,使他们能够更新自己的状态。观察者模式中存在两种角色:观察者:内部包含被观察者对象,当被观察者对象的状态发生变化时,更新自己的状态。(接收通知更新状态)被观察者:内部包含了所有观察者对象,当状态发生变化时通知所有的观察者更新自己的状态。(发送通知)应用场景:当一个对象的改变需要同时改变其他对象,且不知道具体有多少对象有待改变时,应该考虑使用观察者模式;一个抽象模型有两个方面,其中一方面依赖于另一方面,这时可以用观察者模式将这两者封装在独立的对象中使它们各自独立地改变和复用。实现方式:C++#include <iostream> #include <string> #include <list> using namespace std; class Subject; //观察者 基类 (内部实例化了被观察者的对象sub) class Observer { protected: string name; Subject *sub; public: Observer(string name, Subject *sub) { this->name = name; this->sub = sub; } virtual void update() = 0; }; class StockObserver : public Observer { public: StockObserver(string name, Subject *sub) : Observer(name, sub) { } void update(); }; class NBAObserver : public Observer { public: NBAObserver(string name, Subject *sub) : Observer(name, sub) { } void update(); }; //被观察者 基类 (内部存放了所有的观察者对象,以便状态发生变化时,给观察者发通知) class Subject { protected: list<Observer *> observers; public: string action; //被观察者对象的状态 virtual void attach(Observer *) = 0; virtual void detach(Observer *) = 0; virtual void notify() = 0; }; class Secretary : public Subject { void attach(Observer *observer) { observers.push_back(observer); } void detach(Observer *observer) { list<Observer *>::iterator iter = observers.begin(); while (iter != observers.end()) { if ((*iter) == observer) { observers.erase(iter); return; } ++iter; } } void notify() { list<Observer *>::iterator iter = observers.begin(); while (iter != observers.end()) { (*iter)->update(); ++iter; } } }; void StockObserver::update() { cout << name << " 收到消息:" << sub->action << endl; if (sub->action == "梁所长来了!") { cout << "我马上关闭股票,装做很认真工作的样子!" << endl; } } void NBAObserver::update() { cout << name << " 收到消息:" << sub->action << endl; if (sub->action == "梁所长来了!") { cout << "我马上关闭NBA,装做很认真工作的样子!" << endl; } } int main() { Subject *dwq = new Secretary(); Observer *xs = new NBAObserver("xiaoshuai", dwq); Observer *zy = new NBAObserver("zouyue", dwq); Observer *lm = new StockObserver("limin", dwq); dwq->attach(xs); dwq->attach(zy); dwq->attach(lm); dwq->action = "去吃饭了!"; dwq->notify(); cout << endl; dwq->action = "梁所长来了!"; dwq->notify(); return 0; }
C++
0
0
0
浏览量1083
菜鸟码转

24.不允许修改类的成员变量的函数实现方法

如果想达到一个类的成员函数不能修改类的成员变量,只需用 const 关键字来修饰该函数即可。该问题本质是考察 const 关键字修饰成员函数的作用,只不过以实例的方式来考察,面试者应熟练掌握 const 关键字的作用。同时 C++ 还存在与 const 相反的关键字 mutable。被 mutable 修饰的变量,将永远处于可变的状态,即使在一个 const 函数中。如果我们需要在 const 函数中修改类的某些成员变量,这时就需要用到 mutable。使用 mutable 的注意事项:mutable 只能作用于类的非静态和非常量数据成员。在一个类中,应尽量避免大量使用 mutable,大量使用 mutable 表示程序设计存在缺陷。C++#include <iostream> using namespace std; class A { public: mutable int var1; int var2; A() { var1 = 10; var2 = 20; } void fun() const // 不能在 const 修饰的成员函数中修改成员变量的值,除非该成员变量用 mutable 修饰 { var1 = 100; // ok var2 = 200; // error: assignment of member 'A::var1' in read-only object } }; int main() { A ex1; return 0; }我们可以看到在 const 函数中, mutable 修饰的变量可以修改,否则则不能修改。
C++
0
0
0
浏览量1095
菜鸟码转

22.如何禁止一个类被实例化

1.方法一:在类中定义一个纯虚函数,使该类成为抽象基类,因为不能创建抽象基类的实例化对象;C++#include <iostream> using namespace std; class A { public: int var1, var2; A(){ var1 = 10; var2 = 20; } virtual void fun() = 0; // 纯虚函数 }; int main() { A ex1; // error: cannot declare variable 'ex1' to be of abstract type 'A' return 0; }2.方法二:将类的所有构造函数声明为私有 private;3.方法三:C++ 11 以后,将类的所有构造函数用 =delete 修饰;
C++
0
0
0
浏览量1199
菜鸟码转

6.函数指针的定义

1.函数指针:函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。我们知道所有的函数最终的编译都生成代码段,每个函数的都只是代码段中一部分而已,在每个函数在代码段中都有其调用的起始地址与结束地址,因此我们可以用指针变量指向函数的在代码段中的起始地址。C++#include <iostream> using namespace std; int fun1(int tmp1, int tmp2) { return tmp1 * tmp2; } int fun2(int tmp1, int tmp2) { return tmp1 / tmp2; } int main() { int (*fun)(int x, int y); fun = fun1; // ok fun = &fun1; // ok 两种写法均可以 cout << fun(15, 5) << endl; fun = fun2; cout << fun(15, 5) << endl; cout<<sizeof(fun1)<<endl; // error cout<<sizeof(&fun1)<<endl; return 0; } /* 运行结果: 75 3 */需要注意的是,对于 fun1 和 &fun1:函数名 fun1 存放的是函数的首地址,它是一个函数类型 void,&fun1 表示一个指向函数对象 fun1 的地址,是一个指针类型。它的类型是 int (*)(int,int),因此 fun1 和 &fun1 的值是一样的;&fun1 是一个表达式,函数此时作为一个对象,取对象的地址,该表达式的值是一个指针。通过打印 sizeof 即可知道 fun1 与 &fun1 的区别;
C++
0
0
0
浏览量543
菜鸟码转

8.多重继承的常见问题及避免方法

多重继承(多继承):是指从多个直接基类中产生派生类。多重继承容易出现命名冲突和数据冗余问题。程序示例如下:C++#include <iostream> using namespace std; // 间接基类 class Base1 { public: int var1; }; // 直接基类 class Base2 : public Base1 { public: int var2; }; // 直接基类 class Base3 : public Base1 { public: int var3; }; // 派生类 class Derive : public Base2, public Base3 { public: void set_var1(int tmp) { var1 = tmp; } // error: reference to 'var1' is ambiguous. 命名冲突 void set_var2(int tmp) { var2 = tmp; } void set_var3(int tmp) { var3 = tmp; } void set_var4(int tmp) { var4 = tmp; } private: int var4; }; int main() { Derive d; return 0; }上述程序的继承关系如下:(菱形继承)上述代码中存的问题:对于派生类 Derive 上述代码中存在直接继承关系和间接继承关系。直接继承:Base2 、Base3间接继承:Base1对于派生类中继承的的成员变量 var1 ,从继承关系来看,实际上保存了两份,一份是来自基类 Base2,一份来自基类 Base3。因此,出现了命名冲突。1.解决方法:显式声明出现冲突的成员变量来源于哪个类。#include <iostream> using namespace std; // 间接基类 class Base1 { public: int var1; }; // 直接基类 class Base2 : public Base1 { public: int var2; }; // 直接基类 class Base3 : public Base1 { public: int var3; }; // 派生类 class Derive : public Base2, public Base3 { public: void set_var1(int tmp) { Base2::var1 = tmp; } // 这里声明成员变量来源于类 Base2,当然也可以声明来源于类 Base3 void set_var2(int tmp) { var2 = tmp; } void set_var3(int tmp) { var3 = tmp; } void set_var4(int tmp) { var4 = tmp; } private: int var4; }; int main() { Derive d; return 0; }2.解决方法: 虚继承#include <iostream> using namespace std; // 间接基类,即虚基类 class Base1 { public: int var1; }; // 直接基类 class Base2 : virtual public Base1 // 虚继承 { public: int var2; }; // 直接基类 class Base3 : virtual public Base1 // 虚继承 { public: int var3; }; // 派生类 class Derive : public Base2, public Base3 { public: void set_var1(int tmp) { var1 = tmp; } void set_var2(int tmp) { var2 = tmp; } void set_var3(int tmp) { var3 = tmp; } void set_var4(int tmp) { var4 = tmp; } private: int var4; }; int main() { Derive d; return 0; }类之间的继承关系:由于使用多重继承很容易出现二义性的问题,将使得程序调试和维护工作变得非常复杂,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
C++
0
0
0
浏览量2013
菜鸟码转

9.野指针和悬空指针详解

悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。如果对悬空指针再次释放可能会出现不可预估的错误,比如可能该段内存被别的程序申请使用了,而此时对该段内存进行释放可能会产生不可预估的后果。举例:C++void *p = malloc(size); free(p); // 此时,p 指向的内存空间已释放, p 就是悬空指针。 p = NULL;野指针:“野指针” 是指不确定其指向的指针,未初始化的指针为“野指针”,未初始化的指针的初始值可能是随机的,如果使用未初始化的指针可能会导致段错误,从而程序会崩溃。C++void *p; // 此时 p 是“野指针”。如何避免野指针:指针在定义时即初始化,指针在释放完成后,需要将其置为空。
C++
0
0
0
浏览量2014
菜鸟码转

25.对象创建限制在堆或栈

C++ 中的类的对象的建立分为两种:静态建立、动态建立。如何限制类的对象只能在堆上创建?如何限制对象只能在栈上创建?静态建立:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。例如:A a;动态建立:使用 new 关键字在堆空间上创建对象,底层首先调用 operator new() 函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。例如:A *p = new A();1.限制对象只能建立在堆上:最直观的思想:避免直接调用类的构造函数,因为对象静态建立时,会调用类的构造函数创建对象。但是直接将类的构造函数设为私有并不可行,因为当构造函数设置为私有后,不能在类的外部调用构造函数来构造对象,只能用 new 来建立对象。但是由于 new 创建对象时,底层也会调用类的构造函数,将构造函数设置为私有后,那就无法在类的外部使用 new 创建对象了。因此,这种方法不可行。解决方法 1:将析构函数设置为私有。原因:静态对象建立在栈上,是由编译器分配和释放内存空间,编译器为对象分配内存空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性。当析构函数设为私有时,编译器创建的对象就无法通过访问析构函数来释放对象的内存空间,因此,编译器不会在栈上为对象分配内存。C++class A { public: A() {} void destroy() { delete this; } private: ~A() { } };该方法存在的问题:用 new 创建的对象,通常会使用 delete 释放该对象的内存空间,但此时类的外部无法调用析构函数,因此类内必须定义一个 destroy() 函数,用来释放 new 创建的对象。无法解决继承问题,因为如果这个类作为基类,析构函数要设置成 virtual,然后在派生类中重写该函数,来实现多态。但此时,析构函数是私有的,派生类中无法访问。解决方法 2:构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。C++class A { protected: A() {} ~A() {} public: static A *create() { return new A(); } void destroy() { delete this; } };2.限制对象只能建立在栈上:解决方法:将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。C++class A { private: void *operator new(size_t t) {} // 注意函数的第一个参数和返回值都是固定的 void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete public: A() {} ~A() {} };
C++
0
0
0
浏览量1383
菜鸟码转

1.单例模式及其实现

单例模式:保证类的实例化对象仅有一个,并且提供一个访问他的全局访问点。应用场景:表示文件系统的类,一个操作系统一定是只有一个文件系统,因此文件系统的类的实例有且仅有一个。打印机打印程序的实例,一台计算机可以连接好几台打印机,但是计算机上的打印程序只有一个,就可以通过单例模式来避免两个打印作业同时输出到打印机。实现方式:单例模式可以通过全局或者静态变量的形式实现,这样比较简单,但是这样会影响封装性,难以保证别的代码不会对全局变量造成影响。默认的构造函数、拷贝构造函数、赋值构造函数声明为私有的,这样禁止在类的外部创建该对象;全局访问点也要定义成 静态类型的成员函数,没有参数,返回该类的指针类型。因为使用实例化对象的时候是通过类直接调用该函数,并不是先创建一个该类的对象,通过对象调用。不安全的实现方式:原因:考虑当两个线程同时调用 getInstance 方法,并且同时检测到 instance 是 NULL,两个线程会同时实例化对象,不符合单例模式的要求。C++class Singleton{ private: static Singleton * instance; Singleton(){} Singleton(const Singleton& tmp){} Singleton& operator=(const Singleton& tmp){} public: static Singleton* getInstance(){ if(instance == NULL){ instance = new Singleton(); } return instance; } }; Singleton* Singleton::instance = NULL;分类:懒汉模式:直到第一次用到类的实例时才去实例化,上面是懒汉实现。饿汉模式:类定义的时候就实例化。线程安全的懒汉模式实现:方法:加锁存在的问题:每次判断实例对象是否为空,都要被锁定,如果是多线程的话,就会造成大量线程阻塞。C++class Singleton{ private: static pthread_mutex_t mutex; static Singleton * instence; Singleton(){ pthread_mutex_init(&mutex, NULL); } Singleton(const Singleton& tmp){} Singleton& operator=(const Singleton& tmp){} public: static Singleton* getInstence(){ pthread_mutex_lock(&mutex); if(instence == NULL){ instence = new Singleton(); } pthread_mutex_unlock(&mutex); return instence; } }; Singleton* Singleton::instence = NULL; pthread_mutex_t Singleton::mutex;方法:内部静态变量,在全局访问点 getInstance 中定义静态实例。C++class Singleton{ private: static pthread_mutex_t mutex; Singleton(){ pthread_mutex_init(&mutex, NULL); } Singleton(const Singleton& temp){} Singleton& operator=(const Singleton& temp){} public: static Singleton* getInstence(){ static Singleton instence; return &instence; } }; pthread_mutex_t Singleton::mutex; 饿汉模式的实现:饿汉模式本身就是线程安全的不用加锁。C++class Singleton{ private: static Singleton* instence; Singleton(const Singleton& temp){} Singleton& operator=(const Singleton& temp){} protected: Singleton(){} public: static Singleton* getInstence(){ return instence; } }; Singleton* Singleton::instence = new Singleton();
C++
1
0
0
浏览量20
菜鸟码转

4.常见设计模式

《大话设计模式》一书中提到 24 种设计模式,这 24 种设计模式没必要面面俱到,但一定要深入了解其中的几种,最好结合自己在实际开发过程中的例子进行深入的了解。设计模式有 6 大设计原则:单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。开放封闭原则:软件实体可以扩展,但是不可修改。即面对需求,对程序的改动可以通过增加代码来完成,但是不能改动现有的代码。里氏代换原则:一个软件实体如果使用的是一个基类,那么一定适用于其派生类。即在软件中,把基类替换成派生类,程序的行为没有变化。依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接口编程,不要针对实现编程。迪米特原则:如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。接口隔离原则:每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。设计模式分为三类:创造型模式:单例模式、工厂模式、建造者模式、原型模式结构型模式:适配器模式、桥接模式、外观模式、组合模式、装饰模式、享元模式、代理模式行为型模式:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式下面介绍常见的几种设计模式:单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。工厂模式:包括简单工厂模式、抽象工厂模式、工厂方法模式简单工厂模式:主要用于创建对象。用一个工厂来根据输入的条件产生不同的类,然后根据不同类的虚函数得到不同的结果。工厂方法模式:修正了简单工厂模式中不遵守开放封闭原则。把选择判断移到了客户端去实现,如果想添加新功能就不用修改原来的类,直接修改客户端即可。抽象工厂模式:定义了一个创建一系列相关或相互依赖的接口,而无需指定他们的具体类。观察者模式:定义了一种一对多的关系,让多个观察对象同时监听一个主题对象,主题对象发生变化时,会通知所有的观察者,使他们能够更新自己。装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成派生类更为灵活。
C++
0
0
0
浏览量858
菜鸟码转

3.C++ 互斥信号量

1.std::thread:我们可以通过 thread 创建一个线程(C++ 11 以后才支持 thread 标准库),thread 在构造相关线程对象完成后立即开始执行(实际需要等待 OS 对于线程的调度延迟)。thread 需要配合与 join 或者 detach 配合使用,不然可能出现不可预料的后果。join 代表阻塞当前主线程,等待当前的 join 的子线程完成后主线程才会继续;detach 表明当前子线程不阻塞主线程,且与主线程分离,子线程的运行不会影响到主线程。C++#include <iostream> #include <thread> #include <string> using namespace std; void fn2(string st2) { cout<<st2<<endl; } void fn1(string st1) { cout<<st1<<endl; } int main() { thread thr1(fn1, "111111111\n"); thread thr2(fn2, "222222222\n"); return 0; }如在一个类中间,一个成员函数需要异步调用另一个函数的时候,需要绑定 this:C++class Test{ private: void func1(const std::string s1, int i) { std::cout << s1 << std::endl; } public: void func2() { std::thread t1(&Test::func1, this, "test", 0); t1.detach(); std::cout << "func2" << std::endl; }2.std::async:我们进行异步编程时,需要得到子进程的计算结果,常见的手段是我们可以通过共享变量或者消息队列的方式告知另一个线程当前的计算结果,但是操作和实现都比较麻烦,同时还要考虑线程间的互斥问题。C++11 中提供了一个相对简单的异步接口 std::async,通过这个接口可以简单地创建线程并通过 std::future 中获取结果,极大的方便了 C++ 多线程编程。std::async 适合与需要取得结果的异步线程。C++11 中的 std::async 是个模板函数。std::async 异步调用函数,在某个时候以 Args 作为参数(可变长参数)调用函数,无需等待函数执行完成就可返回,返回结果是个 std::future 对象。函数返回的值可通过 std::future 对象的 get 成员函数获取。一旦完成函数的执行,共享状态将包含函数返回的值并 ready。async 使用的函数原型和参数说明如下:C++//(C++11 起) (C++17 前) template< class Function, class... Args> std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>> async( Function&& f, Args&&... args ); //(C++11 起) (C++17 前) template< class Function, class... Args > std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>> async( std::launch policy, Function&& f, Args&&... args ); 第一个参数是 std::async 的启动策略类型以下两种方式:async 允许调用者选择特定的启动策略:std::launch::async:在调用 async 就开始创建线程,该函数由新线程异步调用,并且将其返回值与共享状态的访问点同步。std::launch::deferred:延迟启动线程,在访问共享状态时该线程才启动。对函数的调用将推迟到返回的 std::future 的共享状态被访问时(即使用 std::future 的 wait 或 get 函数)。如果 get 或 wait 没有被调用,函数就绝对不会执行。参数 Function:可以为函数指针、成员指针、任何类型的可移动构造的函数对象,也可以为匿名函数 lambda。Function 的返回值或异常存储在共享状态中以供异步的 std::future 对象检索。std::future 对象可以通过 wait 或 get 函数获取函数的返回值。参数 Args:传递给函数 Function 调用的参数,它们的类型应是可移动构造的。我们可以用多线程将程序分为子任务,用子任务求解,程序示例如下:C++#include <iostream> #include <vector> #include <algorithm> #include <numeric> #include <future> #include <string> #include <mutex> template <typename RandomIt> int parallel_sum(RandomIt beg, RandomIt end) { auto len = end - beg; if (len < 1000) return std::accumulate(beg, end, 0); RandomIt mid = beg + len/2; auto handle = std::async(std::launch::async, parallel_sum<RandomIt>, mid, end); int sum = parallel_sum(beg, mid); return sum + handle.get(); } template <typename RandomIt> int parallel_min(RandomIt beg, RandomIt end) { auto len = end - beg; if (len < 1000) return *(std::min_element(beg, end)); RandomIt mid = beg + len/2; auto handle = std::async(std::launch::async, parallel_min<RandomIt>, mid, end); int ans = parallel_sum(beg, mid); return std::min(ans, handle.get()); } int main() { std::vector<int> v(10000, 1); std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n'; std::cout << "The min element is " << parallel_min(v.begin(), v.end()) << '\n'; } 3.std::future:std::future 提供了一种访问线程异步操作结果的机制。从字面意思来看,future 表示未来,std::async 返回结果即为一个 future。在实际工程项目中,一个异步操作我们是不可能马上就获取操作结果的,只能在将来的某个时候获取,但是我们可以以同步等待的方式来获取结果,可以通过查询 future 的状态(future_status)来获取异步操作的结果。future_status 有三种状态:deferred:异步操作还没开始;ready:异步操作已经完成;timeout:异步操作超时;获取 future 结果有三种方式:get、wait、wait_for,其中 get 等待异步操作结束并返回结果,wait 只是等待异步操作完成,没有返回值,wait_for 是超时等待返回结果。C++#include <iostream> #include <future> #include <thread> int main() { // future from an async() std::future<int> f1 = std::async(std::launch::async, []{ return 8; }); // future from a promise std::promise<int> p; std::future<int> f2 = p.get_future(); std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach(); std::cout << "Waiting..." << std::flush; f1.wait(); f2.wait(); std::cout << "Done!\nResults are: " << f1.get() << ' ' << f2.get() << '\n'; } /* Waiting...Done! Results are: 8 9 */
C++
0
0
0
浏览量2028
菜鸟码转

27.类的大小

1.类大小的计算:说明:类的大小是指类的实例化对象的大小,用 sizeof 对类型名操作时,结果是该类型的对象的大小。计算原则如下:遵循结构体的成员变量对齐原则。与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。虚函数对类的大小有影响,是因为虚函数表指针的影响。虚继承对类的大小有影响,是因为虚基表指针带来的影响。空类的大小是一个特殊情况,空类的大小为 1,空类同样可以被实例化,而每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以sizeof(A) 的大小为 1。2.简单情况和空类情况:C++/* 说明:程序是在 64 位编译器下测试的 */ #include <iostream> using namespace std; class A { private: static int s_var; // 不影响类的大小 const int c_var; // 4 字节 int var; // 8 字节 4 + 4 (int) = 8 char var1; // 12 字节 8 + 1 (char) + 3 (填充) = 12 public: A(int temp) : c_var(temp) {} // 不影响类的大小 ~A() {} // 不影响类的大小 }; class B { }; int main() { A ex1(4); B ex2; cout << sizeof(ex1) << endl; // 12 字节 cout << sizeof(ex2) << endl; // 1 字节 return 0; }3.带有虚函数的情况:注意:虚函数的个数并不影响所占内存的大小,因为类对象的内存中只保存了指向虚函数表的指针。由于不同平台、不同编译器厂商所生成的虚表指针在内存中的布局是不同的,有些将虚表指针置于对象内存中的开头处,有些则置于结尾处。在 X64 GCC 编译器下,虚指针在类的开头出,我们可以通过偏移量获取。程序示例,我们通过对象内存的开头处取出 vptr,并遍历对象虚函数表。C++/* 说明:程序是在 64 位编译器下测试的 */ #include <iostream> using namespace std; class A { private: static int s_var; // 不影响类的大小 const int c_var; // 4 字节 int var; // 8 字节 4 + 4 (int) = 8 char var1; // 12 字节 8 + 1 (char) + 3 (填充) = 12 public: A(int temp) : c_var(temp) {} // 不影响类的大小 ~A() {} // 不影响类的大小 virtual void f() { cout << "A::f" << endl; } virtual void g() { cout << "A::g" << endl; } virtual void h() { cout << "A::h" << endl; } // 24 字节 12 + 4 (填充) + 8 (指向虚函数的指针) = 24 }; typedef void (*func)(void); void printVtable(unsigned long *vptr, int offset) { func fn = (func)*((unsigned long*)(*vptr) + offset); fn(); } int main() { A ex1(4); A *p; cout << sizeof(p) << endl; // 8 字节 注意:指针所占的空间和指针指向的数据类型无关 cout << sizeof(ex1) << endl; // 24 字节 unsigned long* vPtr = (unsigned long*)(&ex1); printVtable(vPtr, 0); printVtable(vPtr, 1); printVtable(vPtr, 2); return 0; } /* 8 24 A::f A::g A::h */4.含有虚继承的情况:不包含虚继承的情况,派生类直接继承了基类的成员变量,内存分布如下:C++#include <iostream> using namespace std; class A { public: int a; }; class B : public A { public: int b; void bPrintf() { std::cout << "This is class B" << "\n"; } }; int main(){ A a; B b; cout<<sizeof(a)<<endl; cout<<sizeof(b)<<endl; return 0; } /* 4 8 */如果加入虚继承,此时对象中多了一个指向虚基类表的指针,对象 B 与对象 C 均多了一个指针变量 vbptr。C++#include <iostream> using namespace std; // 采用 4 字节对齐 #pragma pack(4) class A { public: int a; }; class B : virtual public A { public: int b; void bPrintf() { std::cout << "This is class B" << "\n";} }; class C : virtual public A { public: int c; void cPrintf() { std::cout << "This is class C" << "\n";} }; class D : public B, public C { public: int d; void dPrintf() { std::cout << "This is class D" << "\n";} }; int main(){ A a; B b; C c; D d; cout<<sizeof(a)<<endl; cout<<sizeof(b)<<endl; cout<<sizeof(c)<<endl; cout<<sizeof(d)<<endl; return 0; } /* 4 16 16 32 */我们可以看到:实际的内存布局如下:虚基类表的填充内容如下:第一项表示派生类对象指针相对于虚基类表指针 vbptr 的偏移,在图中我们可以看到在 B 中,B 的起始地址相对于 vptr 的偏移量为 12;从第二项开始表示各个基类的地址相对于虚基类表指针 vbptr 的偏移,在图中我们可以看到在 B 中,A 的起始地址相对于 vptr 的偏移量为 12;虚继承的情况就比较复杂,虚继承需要额外加上一个指向虚基类表的指针。虚继承的基础上如果再加上虚函数,还需要额外加上虚函数表的指针占用的空间。
C++
0
0
0
浏览量2015
菜鸟码转

19.C++ 模板编程

模板是 C++ 编程语言的一个特性,它允许函数和类使用泛型类型进行操作。这允许一个函数或类在许多不同的数据类型上工作,而无需为每个类型重写。C++ 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码,C++ 中使用 template 关键字。模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。共有三种模板:函数模板、类模板以及自 C++ 14 以来的变量模板:1.函数模板:函数模板的行为类似于函数,只是模板可以有许多不同类型的参数。一个函数模板代表一个函数族。使用类型参数声明函数模板的格式是:template<class identifier> declaration; template<typename identifier> declaration;上述两种表达方式完全相同,引入后一种时为了防止混淆。比如 C++ 标准库包含 max(x, y) 返回较大的 x 和的函数模板 y 。该函数模板可以这样定义:template<typename T> T max(T &a, T &b) { return a > b ? a : b; } std :: cout << max ( 3 , 7 ) << '\n' ; std :: cout << max ( 3.0 , 7.0 ) << '\n' ; 这个单一的函数定义适用于许多数据类型。具体来说,它适用于定义了 >(大于运算符)的所有数据类型。除了限制对一个函数描述的更改并使代码更易于阅读之外,函数模板的使用减少了源代码的编写,与为特定程序中使用的所有不同数据类型编写单独的函数相比,模板不会产生更小的目标代码,实际编译器在编译时,会为根据不同的类型编译产生不同的函数。2.类模板:类模板提供了基于参数生成类的规范。类模板通常用于实现容器。类模板通过将一组给定的类型作为模板参数传递给它来实例化。C++ 标准库包含许多类模板,特别是改编自标准模板库的容器,例如 vector,list。C++template <class T> class Stack { private: vector<T> elements; // 元素 public: void push(T const&); // 入栈 void pop(); // 出栈 T top() const; // 返回栈顶元素 bool empty() const{ // 如果为空则返回真。 return elements.empty(); } }; 3.变量模板:在 C++14 以后,变量也可以参数化为特定的类型,这称为变量模板。C++template<typename T> constexpr T pi = T{3.141592653589793238462643383L}; // (Almost) from std::numbers::pi使用变量模板时,必须显式地指定它的类型:C++std::cout << pi<double> << '\n'; std::cout << pi<float> << '\n';4.函数重载与模板的区别:函数重载和模板都是面向对象多态特性的例子。当多个函数执行非常相似(不相同)的操作时使用函数重载,当多个函数执行相同操作时使用模板。当模板类或者模板函数中含有静态变量时,则每个模板的实例类型都含有一个静态成员。C++template <class T> class A { public: static T val; }; A<int> a; // 含有静态成员 val; A<string> b; // 含有静态成员 val;
C++
0
0
0
浏览量2015
菜鸟码转

20.如何避免拷贝

最直观的想法是:将类的拷贝构造函数和赋值运算符重载声明为私有 private,但对于类的成员函数和友元函数依然可以调用,达不到完全禁止类的对象被拷贝的目的,而且程序会出现错误,因为未对函数进行定义。1.声明一个基类,具体做法如下。定义一个基类,将其中的拷贝构造函数和赋值运算符重载声明为私有 private。派生类以私有 private 的方式继承基类。class Uncopyable { public: Uncopyable() {} ~Uncopyable() {} private: Uncopyable(const Uncopyable &); // 拷贝构造函数 Uncopyable &operator=(const Uncopyable &); // 赋值运算符 }; class A : private Uncopyable // 注意继承方式 { };简单解释:能够保证,在派生类 A 的成员函数和友元函数中无法进行拷贝操作,因为无法调用基类 Uncopyable 的拷贝构造函数或赋值运算符重载。同样,在类的外部也无法进行拷贝操作。2.拷贝构造函数 =delete 修饰:C++ 11 支持 delete 直接禁用类的成员函数调用。class Uncopyable { public: Uncopyable() {} ~Uncopyable() {} Uncopyable(const Uncopyable &) = delete; // 禁用拷贝构造函数 Uncopyable &operator=(const Uncopyable &) = delete; // 禁用赋值运算符 };
C++
0
0
0
浏览量1915
菜鸟码转

12.C++ 11 nullptr 比 NULL 的优势比较

NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 <cstdlib> 中,即 #define NULL 0。nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。二者相比 nullptr 的优势:有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。C++#include <iostream> #include <cstring> using namespace std; void fun(char const *p) { cout << "fun(char const *p)" << endl; } void fun(int tmp) { cout << "fun(int tmp)" << endl; } int main() { fun(nullptr); // fun(char const *p) /* fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous */ return 0; }
C++
0
0
0
浏览量2017
菜鸟码转

9.深拷贝和浅拷贝的区别

如果一个类拥有资源,该类的对象进行复制时,如果资源重新分配,就是深拷贝,否则就是浅拷贝。深拷贝:该对象和原对象占用不同的内存空间,既拷贝存储在栈空间中的内容,又拷贝存储在堆空间中的内容。浅拷贝:该对象和原对象占用同一块内存空间,仅拷贝类中位于栈空间中的内容。当类的成员变量中有指针变量时,最好使用深拷贝。因为当两个对象指向同一块内存空间,如果使用浅拷贝,当其中一个对象的删除后,该块内存空间就会被释放,另外一个对象指向的就是垃圾内存。浅拷贝实例:C++#include <iostream> using namespace std; class Test { private: int *p; public: Test(int tmp) { this->p = new int(tmp); cout << "Test(int tmp)" << endl; } ~Test() { if (p != NULL) { delete p; } cout << "~Test()" << endl; } }; int main() { Test ex1(10); Test ex2 = ex1; return 0; } /* 运行结果: Test(int tmp) ~Test() */说明:上述代码中,类对象 ex1、ex2 实际上是指向同一块内存空间,对象析构时,ex2 先将内存释放了一次,之后析构对象 ex1 时又将这块已经被释放过的内存再释放一次。对同一块内存空间释放了两次,会导致程序崩溃。深拷贝实例:#include <iostream> using namespace std; class Test { private: int *p; public: Test(int tmp) { p = new int(tmp); cout << "Test(int tmp)" << endl; } ~Test() { if (p != NULL) { delete p; } cout << "~Test()" << endl; } Test(const Test &tmp) // 定义拷贝构造函数 { p = new int(*tmp.p); cout << "Test(const Test &tmp)" << endl; } }; int main() { Test ex1(10); Test ex2 = ex1; return 0; } /* Test(int tmp) Test(const Test &tmp) ~Test() ~Test() */编译器生成的默认拷贝函数均大部分都是浅拷贝,所有在特定场景下需要禁止编译器生成默认拷贝构造函数。在遇到需要使用堆内存的构造函数中,我们需要特别注意浅拷贝和深拷贝的使用方式,防止两个不同的对象指向同一块内存区域。
C++
0
0
0
浏览量2015
菜鸟码转

13.如何减少构造函数开销

在构造函数时尽量使用类初始化列表,会减少调用默认的构造函数产生的开销,具体原因可以参考本章《为什么用成员初始化列表会快一些?》这个问题。C++class A { private: int val; public: A() { cout << "A()" << endl; } A(int tmp) { val = tmp; cout << "A(int " << val << ")" << endl; } }; class Test1 { private: A ex; public: Test1(): ex(1) // 成员列表初始化方式 { } };
C++
0
0
0
浏览量2017
菜鸟码转

15.成员初始化列表效率高的原因

对象的成员函数数据类型可分为语言内置类型和用户自定义类,对于用户自定义类型,利用成员初始化列表效率高。用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;如果在构造函数中初始化,由于 C++ 规定对象的成员变量的初始化动作发生在进入自身的构造函数本体之前,那么在执行构造函数之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,再显式调用该成员变量对应的构造函数。因此使用列表初始化会减少调用默认的构造函数的过程,效率更高一些。C++#include <iostream> using namespace std; class A { private: int val; public: A() { cout << "A()" << endl; } A(int tmp) { val = tmp; cout << "A(int " << val << ")" << endl; } }; class Test1 { private: A ex; public: Test1() : ex(1) // 成员列表初始化方式 { } }; class Test2 { private: A ex; public: Test2() // 函数体中赋值的方式 { ex = A(2); } }; int main() { Test1 ex1; cout << endl; Test2 ex2; return 0; } /* 运行结果: A(int 1) A() A(int 2) */说明:从程序运行结果可以看出,使用成员列表初始化的方式会省去调用默认的构造函数的过程。如果自定义的类型没有默认构造函数,此时必须使用初始化列表提供初值对这些类型进行初始化。
C++
0
0
0
浏览量2016
菜鸟码转

2.重载、重写、隐藏的区别

1.函数重载:重载是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。C++class A { public: void fun(int tmp); void fun(float tmp); // 重载 参数类型不同(相对于上一个函数) void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数) void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数) int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型 };2.函数隐藏:函数隐藏是指派生类的函数屏蔽了与其同名的基类函数,只要是与基类同名的成员函数,不管参数列表是否相同,基类函数都会被隐藏。C++#include <iostream> using namespace std; class Base { public: void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; } }; class Derive : public Base { public: void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数 }; int main() { Derive ex; ex.fun(1); // Derive::fun(int tmp) ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided return 0; }说明: 上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。3.函数重写(覆盖):函数覆盖是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。C++#include <iostream> using namespace std; class Base { public: virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; } }; class Derived : public Base { public: virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数 }; int main() { Base *p = new Derived(); p->fun(3); // Derived::fun(int) : 3 return 0; }4.重写和重载的区别:范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。5.隐藏和重写,重载的区别:范围区别:隐藏与重载范围不同,隐藏发生在不同类中。参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。利用重写可以实现多态,而隐藏不可以。如果使用基类指针 p 指向派生类对象,利用这个指针调用函数时,对于隐藏的函数,会根据指针的类型去调用函数;对于重写的函数,会根据指针所指对象的类型去调用函数。重写必须使用 virtual 关键字,此时会更改派生类虚函数表的表项。隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口。
C++
0
0
0
浏览量2029
菜鸟码转

14.模板及其实现

1.模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。模板参数列表不能为空;模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。C++template <typename T, typename U, ...>2.函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。C++#include<iostream> using namespace std; template <typename T> T add_fun(const T & tmp1, const T & tmp2){ return tmp1 + tmp2; } int main(){ int var1, var2; cin >> var1 >> var2; cout << add_fun(var1, var2); double var3, var4; cin >> var3 >> var4; cout << add_fun(var3, var4); return 0; }3.类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。C++#include <iostream> using namespace std; template <typename T> class Complex { public: //构造函数 Complex(T a, T b) { this->a = a; this->b = b; } //运算符重载 Complex<T> operator+(Complex &c) { Complex<T> tmp(this->a + c.a, this->b + c.b); cout << tmp.a << " " << tmp.b << endl; return tmp; } private: T a; T b; }; int main() { Complex<int> a(10, 20); Complex<int> b(20, 30); Complex<int> c = a + b; return 0; }4.变量模板:在 C++14 以后,变量也可以参数化为特定的类型,这称为变量模板。C++template<typename T> constexpr T pi = T{3.141592653589793238462643383L}; // (Almost) from std::numbers::pi使用变量模板时,必须显式地指定它的类型:C++std::cout << pi<double> << '\n'; std::cout << pi<float> << '\n';5.函数重载与模板的区别:函数重载和模板都是面向对象多态特性的例子。当多个函数执行非常相似(不相同)的操作时使用函数重载,当多个函数执行相同操作时使用模板,函数模板也可以重载。当模板类或者模板函数中含有静态变量时,则每个模板的实例类型都含有一个静态成员。C++template <class T> class A { public: static T val; }; A<int> a; // 含有静态成员 int val; A<string> b; // 含有静态成员 string val;
C++
0
0
0
浏览量2022
菜鸟码转

1.面向对象及其三大特性

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。面向对象的三大特性:封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在 C++ 中多态一般是使用虚函数来实现的,使用基类指针调用函数方法时,如果该指针指向的是一个基类的对象,则调用的是基类的虚函数;如果该指针指向的是一个派生类的对象,则调用的是派生类的虚函数。
C++
0
0
0
浏览量2030
菜鸟码转

1.左值和右值:区别、引用及转化

1.左值与右值:左值:指表达式结束后依然存在的持久对象。可以取地址,可以通过内置(不包含重载) & 来获取地址,我们可以将一个右值赋给左值。右值:表达式结束就不再存在的临时对象。不可取地址,不可以通过内置(不包含重载) & 来获取地址。由于右值不可取地址,因此我们不能将任何值赋给右值。使用 = 进行赋值时,= 的左边必须为左值,右值只能出现在 = 的右边。程序示例:C++// x 是左值,666 为右值 int x = 666; // ok int *y = x; // ok int *z = &666 // error 666 = x; // error int a = 9; // a 为左值 int b = 4; // b 为左值 int c = a + b // c 为左值 , a + b 为右值 a + b = 42; // error函数返回值即可以是左值,也可以是右值:C++int setValue() { return 6; } int global = 100; int& setGlobal() { return global; } setValue() = 3; // error! setGlobal() = 400; // OK2.左值引用和右值引用:引用的定义在之前的章节中已经介绍过。左值引用:左值引用可以区分为常量左值引用和非常量左值引用。左值引用的底层实现是指针实现。非常量左值引用只能绑定到非常量左值,不能绑定到常量左值和右值。如果绑定到非常量右值,就有可能指向一个已经被销毁的对象。常量左值引用能绑定到非常量左值,常量左值和右值;C++int y = 10; int& yref = y; // ok int& xref = 10; // error, 非常量左值引用绑定右值 const &xref = 10; // ok, 常量左值引用绑定右值 int a = 10; int b = 20; int& zref = a + b // error, a + b为右值 int &aref1 = a; //ok, 非常量左值引用绑定非常量左值 const int &aRef2 = a; //ok, 常量左值引用绑定非常量左值 const int c = 4; int &cref1 = c; // error,非常量左值不能绑定常量右值 const int &cref2 = c; //ok, 常量左值引用绑定常量左值 const int &ref2 = a + b; //ok, 常量左值引用绑定到右值(表达式)我们来观察一下函数运行:如果函数的形参定义为非常量的左值引用,则会出现错误,因为此时我们将一个左值引用绑定到右值上:C++void fnc(int& x) { } int main() { fnc(10); // error! }如果函数的形参定义为常量的左值引用,则可以正常运行,因为此时我们将一个常量左值引用绑定到一个右值上:C++void fnc(const int& x) { } int main() { int x = 10; fnc(x); // ok! fnc(10); // ok! }右值引用:右值引用 (Rvalue Referene) 是 C++ 11 中引入的新特性 , 它实现了转移语义 (Move Sementics)和精确传递 (Perfect Forwarding),&& 作为右值引用的声明符。右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。从实践角度讲,它能够完美解决 C++ 中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了 C++ 中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从使用者的角度来看,可以获得效率的提升,避免对象在传递过程中重复创建。右值引用两个主要功能:消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。能够更简洁明确地定义泛型函数。C++#include <iostream> using namespace std; int g_val = 10; void ProcessValue(int &i) { // 左值引用 cout << "lValue processed: " << i << endl; } void ProcessValue(int &&i) { // 右值引用 cout << "rValue processed: " << i << endl; } int GetValue() { // 返回右值 return 3; } int& getVal() { // 返回左值引用 return g_val; } int main() { int a = 0; int b = 1; int &alRef = a; // 左值引用 int &&rRef1 = 1; // 临时对象是右值 int &&rRef2 = GetValue(); // 调用的函数为右值 ProcessValue(a); // 左值 ProcessValue(getVal()); // 左值引用 ProcessValue(1); // 临时对象是右值 ProcessValue(GetValue()); // 调用的函数为右值 ProcessValue(a+b); // 表达式为右值 return 0; } /* lValue processed: 0 lValue processed: 10 rValue processed: 1 rValue processed: 3 rValue processed: 1 */有了右值引用后,函数调用可以写为如下,此时我们用右值引用绑定到右值上:C++void fnc(int&& x) { } int main() { int x = 10; fnc(x); // error, 右值引用不能绑定到左值上 fnc(10); // ok! }3.左值转换成右值:左值转换为右值我们可以通过 std::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义,从而完成将资源的所有权进行转移。C++#include <iostream> using namespace std; void fun(int& tmp) { cout << "fun lvalue bind:" << tmp << endl; } void fun(int&& tmp) { cout << "fun rvalue bind:" << tmp << endl; } void fun1(int& tmp) { cout << "fun1 lvalue bind:" << tmp << endl; } int main() { int var = 11; fun(12); // 右值引用 fun(var); // 左值引用 fun(std::move(var)); // 使用std::move转为右值引用 fun(static_cast<int&&>(var)); // 使用static_cast转为右值引用 fun((int&&)var); // 使用C风格强转为右值引用 fun(std::forward<int&&>(var)); // 使用std::forwad<T&&>为右值引用 fun1(12); // error return 0; } /* fun rvalue bind:12 fun lvalue bind:11 fun rvalue bind:11 fun rvalue bind:11 fun rvalue bind:11 fun rvalue bind:11 */4.引用折叠:通过类型别名或者通过模板参数间接定义,多重引用最终折叠成左值引用或者右值引用。有两种引用(左值和右值),所以就有四种可能的引用+引用的组合(左值 + 左值,左值 + 右值,右值 + 左值,右值 + 右值)。如果引用的引用出现在允许的语境,该双重引用会折叠成单个引用,规则如下:所有的右值引用叠加到右值引用上仍然还是一个右值引用;T&& && 折叠成 T&&所有的其他引用类型之间的叠加都将变成左值引用。T& &&,T&& &, T&& 折叠成 T&。C++#include <iostream> using namespace std; typedef int& lref; typedef int&& rref; void fun(int&& tmp) { cout << "fun rvalue bind:" << tmp << endl; } void fun(int& tmp) { cout << "fun lvalue bind:" << tmp << endl; } int main() { int n = 11; fun((lref&)n); fun((lref&&)n); fun((rref&)n); fun((rref&&)n); return 0; } /* fun lvalue bind:11 fun lvalue bind:11 fun lvalue bind:11 fun rvalue bind:11 */5.万能引用类型:在模板中 T&& t 在发生自动类型推断的时候,它是未定的引用类型(universal references),它既可以接受一个左值又可以接受一个右值。如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。示例代码如下:C++template<typename T> void f(T&& param); template<typename T> class Test { Test(Test&& rhs); };对于函数 template<typename T>void f(T&& t),当参数为右值 10 的时候,根据 universal references 的特点,t 被一个右值初始化,那么 t 就是右值;当参数为左值 x 时,t 被一个左值引用初始化,那么 t 就是一个左值。上面的例子中,param 是 universal reference,rhs 是 Test&& 右值引用,因为模版函数 f 发生了类型推断,而 Test&& 并没有发生类型推导,因为 Test&& 是确定的类型了。正是因为右值引用可能是左值也可能是右值,依赖于初始化,我们可以利用这一点来实现移动语义和完美转发。
C++
0
0
0
浏览量2025
菜鸟码转

17.泛型编程如何实现

泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。模板:可参考本章节中的模板相关问题。泛型编程优缺点:通用性强:泛型算法是建立在语法一致性上,运用到的类型集是无限的/非绑定的。效率高:编译期能确定静态类型信息,其效率与针对某特定数据类型而设计的算法相同。类型检查严:静态类型信息被完整的保存在了编译期,在编译时可以发现更多潜在的错误。二进制复用性差:泛型算法是建立在语法一致性上,语法是代码层面的,语法上的约定无法体现在机器指令中。泛型算法实现的库,其源代码基本上是必须公开的,引用泛型中库都需要重新编译生成新的机器指令。而传统的 C 库全是以二进制目标文件形式发布的,需要使用这些库时直接动态链接加载使用即可,不需要进行再次编译。
C++
0
0
0
浏览量2026
菜鸟码转

26.空类字节数及对应生成的成员函数

1.空类声明时编译器不会生成任何成员函数:对于空类,声明编译器不会生成任何的成员函数,只会生成 1 个字节的占位符。由于在实际程序中,空类同样可以被实例化,而每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以 sizeof(A) 的大小为 1。C++#include <iostream> using namespace std; class A { }; int main() { A a; cout << "sizeof(A):" << sizeof(a) << endl; // sizeof(A):1 return 0; }2.空类定义时编译器会生成 6 个成员函数:当空类 A 定义对象时,sizeof(A) 仍是为 1,但编译器会在需要时生成 6 个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符。C++#include <iostream> using namespace std; /* class A {}; 该空类的等价写法如下: */ class A { public: A(){}; // 缺省构造函数 A(const A &tmp){}; // 拷贝构造函数 ~A(){}; // 析构函数 A &operator=(const A &tmp){}; // 赋值运算符 A *operator&() { return this; }; // 取址运算符 const A *operator&() const { return this; }; // 取址运算符(const 版本) }; int main() { A *p = new A(); cout << "sizeof(A):" << sizeof(A) << endl; // sizeof(A):1 delete p; return 0; }
C++
0
0
0
浏览量645
菜鸟码转

1.C++ 条件变量

1.条件变量(condition variable):在 C 语言中我们使用 pthread_cond_wait 函数作为条件变量,它是由操作系统实现的条件变量,需要详细了解它的运行机制就可以了解 C++ 中条件变量的实现。在 C++ 11 以后,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。C++ 中包含的头文件在 #include <condition_variable> 中。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程因等待条件变量的条件成立而挂起;另外一个线程使条件成立从而给出唤醒线程的信号,从而唤醒被等待的线程;为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起;通常情况下这个锁是 std::mutex,并且管理这个锁只能是 std::unique_lock std::mutex 等 RAII 模板类。分别是使用以下两个方法实现:等待条件成立使用的是 condition_variable 类成员 wait、wait_for 或 wait_until。唤醒信号使用的是 condition_variable 类成员 notify_one 或者 notify_all 函数。condition_variable 支持的函数如下:构造函数: 它只有默认构造函数,拷贝构造函数和赋值符号重载均被禁止 condition_variable(const condition_variable&) = delete;,operator= [delete];wait:wait 目前支持 wait,wait_for,wait_until 等三种操作,分别对应不同的场景:wait: 对应的函数原型为:C++void wait( std::unique_lock<std::mutex>& lock ); 当前线程执行时就被阻塞,直到等到被 notify 唤醒。在阻塞线程的那一刻,该函数自动调用 lck.unlock(),允许其他被 lck 锁定的线程继续运行。阻塞时被一旦某个线程 notify 时,实际可能为虚假唤醒,该函数将解除阻塞并调用 lck.lock(),获取互斥锁。C++template <class Predicate> void wait (unique_lock<mutex>& lck, Predicate pred);调用时检查 pred,如果为 false, 则阻塞线程,并且调用 lock.unlock(), 否则,继续执行。阻塞时被一旦某个线程 notify 时,实际可能为虚假唤醒,该函数将再次检查 pred,如果为 true,则解除阻塞并调用 lck.lock(),获取互斥锁;否则继续阻塞线程。wait_for: 对应的函数原型为:C++template< class Rep, class Period, class Predicate > bool wait_for( std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time, Predicate stop_waiting);调用时,检查两个条件是否满足: stop_waiting 返回是否为 true,时间是否超时,如果两个条件都不满足,则阻塞线程,并调用 lock.unlock(), 否则,到达一定等待时间或满足条件被唤醒。注意等待超过时间段后自动唤醒,判断条件一般需要使用者自己在合适的时候判断,并通过 notify_one() 或 notify_all() 唤醒,所以在使用时应当通过判断返回值来检测是其由于超时返回还是被正常唤醒,即状态是否为 std::cv_status::timeout。程序示例如下:C++#include <iostream> #include <atomic> #include <condition_variable> #include <thread> #include <chrono> using namespace std::chrono_literals; std::condition_variable cv; std::mutex cv_m; int i; void waits(int idx) { std::unique_lock<std::mutex> lk(cv_m); if (cv.wait_for(lk, idx*100ms, []{return i == 1;})) { std::cerr << "Thread " << idx << " finished waiting. i == " << i << '\n'; } else { std::cerr << "Thread " << idx << " timed out. i == " << i << '\n'; } } void signals() { std::this_thread::sleep_for(120ms); std::cerr << "Notifying...\n"; cv.notify_all(); std::this_thread::sleep_for(100ms); { std::lock_guard<std::mutex> lk(cv_m); i = 1; } std::cerr << "Notifying again...\n"; cv.notify_all(); } int main() { std::thread t1(waits, 1), t2(waits, 2), t3(waits, 3), t4(signals); t1.join(); // t1 等待 100ms 后未被唤醒,自动超时; t2.join(); // t2 在 120 ms 处被唤醒,但 condition 未满足,再此进入阻塞,200ms 时由于超时返回 t3.join(); // t3 在 120 ms 处被唤醒,但 condition 未满足,再此进入阻塞,220ms 时被 notify ,正常返回。 t4.join(); } /* Thread 1 timed out. i == 0 Notifying... Thread 2 timed out. i == 0 Notifying again... Thread 3 timed out. i == 0 */wait_until: 对应的函数原型为:C++template <class Predicate> void wait (unique_lock<mutex>& lck, Predicate pred);wait_for 是通过 wait_until 实现的,到达指定截止时间或满足条件 conditions 时线程即被唤醒。不同的是,到达时间点是自动唤醒,而条件满足 conditions 时则是通过 notify_one() 或 notify_all() 唤醒,使用的时候注意判断返回值,根据返回值确定该线程是被超时唤醒还是被其他线程 notify 正常唤醒,即状态是否为 std::cv_status::timeout。notify:notify_one 唤醒一个阻塞的线程,线程唤醒的顺序可能是随机的,并没有特定的顺序。notify_one 程序示例如下,我们可以看到每次唤醒的顺序并不是确定的:C++// condition_variable::notify_one #include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <condition_variable> // std::condition_variable std::mutex mtx; std::condition_variable produce,consume; int cargo = 0; // shared value by producers and consumers void consumer () { std::unique_lock<std::mutex> lck(mtx); while (cargo==0) consume.wait(lck); std::cout << cargo << '\n'; cargo=0; produce.notify_one(); } void producer (int id) { std::unique_lock<std::mutex> lck(mtx); while (cargo!=0) produce.wait(lck); cargo = id; consume.notify_one(); } int main () { std::thread consumers[10],producers[10]; // spawn 10 consumers and 10 producers: for (int i=0; i<10; ++i) { consumers[i] = std::thread(consumer); producers[i] = std::thread(producer,i+1); } // join them back: for (int i=0; i<10; ++i) { producers[i].join(); consumers[i].join(); } return 0; }notify_all 唤醒所有的阻塞在 condition_variable 下的线程。需要特别注意的是,可能唤醒所有阻塞的线程,但是唤醒的多个线程同一时间只能有一个线程可以持有 lck 锁,必须等待其执行 wait 之后的代码,并释放 lck 时,剩余被唤醒的线程才可以执行,我们可以观察到以下程序示例,唤醒的顺序都是随机的。C++#include <iostream> #include <condition_variable> #include <thread> #include <chrono> std::condition_variable_any cv; std::mutex cv_m; // This mutex is used for three purposes: // 1) to synchronize accesses to i // 2) to synchronize accesses to std::cerr // 3) for the condition variable cv int i = 0; void waits(int idx) { std::unique_lock<std::mutex> lk(cv_m); std::cerr << "Waiting... \n"; cv.wait(lk, []{return i == 1;}); std::cerr << "thread:"<< idx <<" ...finished waiting. i == 1\n"; } void signals() { std::this_thread::sleep_for(std::chrono::seconds(1)); { std::lock_guard<std::mutex> lk(cv_m); std::cerr << "Notifying...\n"; } cv.notify_all(); std::this_thread::sleep_for(std::chrono::seconds(1)); { std::lock_guard<std::mutex> lk(cv_m); i = 1; std::cerr << "Notifying again...\n"; } cv.notify_all(); } int main() { std::thread t1(waits, 1), t2(waits, 2), t3(waits, 3), t4(signals); t1.join(); t2.join(); t3.join(); t4.join(); }2.虚假唤醒以及如何避免虚假唤醒:当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件未得到满足时,就会发生虚假唤醒。发生虚假唤醒通常是因为在发出条件变量信号和等待线程最终运行之间,另一个线程运行并更改了条件,导致 wait 的线程被唤醒后,实际条件却未满足。比如我们在 notify_all() 时多个线程都被唤醒,但此时实际共享区却只有少数几个线程可以操作,这时就会造成其他线程被虚假唤醒,可以在 wait 唤醒后再次进行检测 condition 解决虚假唤醒。虚假唤醒导致的后果:之前要等待的某个条件实际上并没有符合,被唤醒的线程将可能会执行一系列非法操作。通常的解决办法:在条件变量阻塞的代码处增加一个 while 循环,如果被唤醒就要检查一下条件是否符合,如果不符合则要再次进入阻塞等待。这样即避免了忙等待,又避免了虚假唤醒问题。C++lock(mutex); while( something not true ) { condition_wait( cond, mutex); } do(something); unlock(mutex);
C++
0
0
0
浏览量2068
菜鸟码转

16.友元函数的作用及使用场景

友元函数的作用:友元(friend)提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个普通的函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。使用场景:1.普通函数定义为类的友元函数,使得普通函数能够访问该类的私有成员和保护成员。C++#include <iostream> using namespace std; class A { friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数 public: A(int tmp) : var(tmp) { } private: int var; }; ostream &operator<<(ostream &_cout, const A &tmp) { _cout << tmp.var; return _cout; } int main() { A ex(4); cout << ex << endl; // 4 return 0; }2.友元类:由于类的 private 和 protected 成员变量只能由类的成员函数访问或者派生类访问,友元类则提供提供一种通用的方法,使得不同类之间可以访问其 private 和 protected 成员变量,用于不同类之间共享数据。C++#include <iostream> using namespace std; class A { friend class B; public: A() : var(10){} A(int tmp) : var(tmp) {} void fun() { cout << "fun():" << var << endl; } private: int var; }; class B { public: B() {} void fun() { cout << "fun():" << ex.var << endl; // 访问类 A 中的私有成员 } private: A ex; }; int main() { B ex; ex.fun(); // fun():10 return 0; }
C++
0
0
0
浏览量2013
菜鸟码转

4.指针和引用的区别

指针:指针是一个变量,它保存另一个变量的内存地址。需要使用 * 运算符指针才能访问它指向的内存位置。引用:引用变量是别名,即已存在变量的另一个名称。对于编译器来说,引用和指针一样,也是通过存储对象的地址来实现的。实际可以将引用视为具有自动间接寻址的常量指针,编译器自动为引用使用 * 运算符。1.二者的区别是否可变:针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦初始化绑定就不能改变。是否占内存:指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间(实际底层编译器可能用指针实现的引用),当我们使用 & 对引用取地址时,将会得到绑定对象的地址。C++#include <iostream> using namespace std; int main() { int a = 10; int &b = a; cout<<&a<<endl; cout<<&b<<endl; return 0; }是否可为空:指针可以定义时不用初始化直接悬空,但是引用初始化时必须绑定对象。是否能为多级指针可以有多级,但是引用只能一级。我们可以定义指针的指针,但不能定义引用的引用。
C++
0
0
0
浏览量465
菜鸟码转

7.参数传递中:值传递、引用传递、指针传递的区别

1.参数传递的三种方式:值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入。当函数内部可能需要改变参数具体的内容时,我们则采用形参,在组成原理上来说,对于值传递的方式我们采用直接寻址。指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后实参和形参是不同的指针,但指向的地址都相同。通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。在组成原理上来说,对于指针传递的方式一般采用间接寻址。引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。当然不同的编译器对于引用有不同的实现,部分编译器在底层也是使用指针来实现引用传递。C++#include <iostream> using namespace std; void fun1(int tmp){ // 值传递 cout << &tmp << endl; } void fun2(int * tmp){ // 指针传递 cout << tmp << endl; } void fun3(int &tmp){ // 引用传递 cout << &tmp << endl; } int main() { int var = 5; cout << "var 在主函数中的地址:" << &var << endl; cout << "var 值传递时的地址:"; fun1(var); cout << "var 指针传递时的地址:"; fun2(&var); cout << "var 引用传递时的地址:"; fun3(var); return 0; } /* 运行结果: var 在主函数中的地址:0x23fe4c var 值传递时的地址:0x23fe20 var 指针传递时的地址:0x23fe4c var 引用传递时的地址:0x23fe4c */说明:从上述代码的运行结果可以看出,只有在值传递时,形参和实参的地址不一样,在函数体内操作的不是变量本身。引用传递和指针传递,在函数体内操作的是变量本身。我们知道函数调用的方式,大部分的编译器按照函数形参定义的逆序,依次将参数压入栈内,上述提到参数的形式,如果是值传递,则压入栈中的是一个临时变量,该变量与传入的值内容相同;如果是指针传递或者引用传递,则压入栈的可能是一个临时的指针变量,该指针指向与传入的指针指向的内容相同。从函数调用机制来开,不管何种调用所有实参的传入时都在栈中开辟了空间。
C++
0
0
0
浏览量1255
菜鸟码转

18.switch 的 case 里为何不建议定义变量

switch 下面的这个花括号表示一块作用域,而不是每一个 case 表示一块作用域。如果在某一 case 中定义了变量,其作用域在这块花括号内,按理说在另一个 case 内可以使用该变量,但是在实际使用时,每一个 case 之间互不影响,是相对封闭的,参考如下实例。实例:下述代码中,在 switch 的 case 中定义的变量,没有实际意义,仅为了解释上述原因。C++#include <iostream> using namespace std; int main() { // 局部变量声明 char var = 'D'; switch (var) { case 'A': int cnt = 0; // 定义变量 cout << "Excellent." << endl << cnt; break; case 'B': case 'C': ++cnt; cout << "Good." << endl << cnt; break; case 'D': cout << "Not bad." << endl << cnt; break; case 'F': cout << "Bad." << endl << cnt; break; default: cout << "Bad." << endl << cnt; } return 0; }简单解释:上述代码中在符合 A 的条件下定义了变量,当符合 B 或者 C 的条件时,对该变量进行自增操作,但是因为不符合条件 A 未对变量进行定义,该变量无法使用。
C++
0
0
0
浏览量2025
菜鸟码转

11.什么是类型萃取

类型萃取(type traits)使用模板技术来萃取类型(包含自定义类型和内置类型)的某些特性,用以判断该类型是否含有某些特性,从而在泛型算法中来对该类型进行特殊的处理用来提高效率或者得到其他优化。简单的来说类型萃取即确定变量去除引用修饰以后的真正的变量类型或者 CV 属性。C++ 关于 type traits 的详细使用技巧可以参考头文件 #include <type_traits>。为什么需要 type traits:对于普通的变量来说,确定变量的类型比较容易,比如 int a = 10; 可以很容易确定变量的实际类型为 int,但在使用模板时确定变量的类型就比较困难,模板传入的类型为不确定性。为什么需要确定变量的实际类型?因为模板函数针对传入的对不同的类型可能作出不同的处理,这就需要我们在处理函数模板对传入的参数类型和特性进行提取。比如自定义拷贝函数 copy(T *dest, const T *src) ,如果 T 此时为 int 类型,则此时我们只需要 *dest = *src 即可,但是如果我们此时传入的 T 为 char * 字符串类型时,则就不能简单进行指针赋值,所以函数在实际处理时则需要对传入的类型进行甄别,从而针对不同的类型给予不同的处理,这样才能使得函数具有通用性。remove_reference_t 的原理:move 函数在进行强制类型转换时,会使用到 remove_reference_t,该函数的作用是确定函数除去 C-V 和引用后的类型。以下为 move 的具体实现:C++template<typename T> remove_reference_t<T>&& move(T&& t) { return static_cast<remove_reference_t<T>&&>(t); }通过 remove_reference_t<T> 可以把 t 对应的类型上的引用给去掉,然后把 t 对应的类型的右值引用符号 && 强制绑定在变量 t 上,这样就强制将变量 t 转换为右值引用类型。remove_reference 函数的原型如下:C++/// remove_reference template<typename _Tp> struct remove_reference { typedef _Tp type; }; template<typename _Tp> struct remove_reference<_Tp&> { typedef _Tp type; }; template<typename _Tp> struct remove_reference < _Tp&& > { typedef _Tp type; };函数的实现非常简单,去掉绑定在类型中的引用,返回一个 实际类型 type。C++ 类型萃取一般用于模板中,当我们定义一个模板函数后,需要知道模板类型形参并加以运用时就奥数可以用类型萃取。通过确定变量的特征我们可以在模板中使用不同的处理方法。
C++
0
0
0
浏览量1646
菜鸟码转

10.强制类型转换的类型

1.static_cast:static_cast 是“静态转换”的意思,也即在编译期间转换,转换失败的话会抛出一个编译错误。一般用于如下:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。用于基本数据类型的转换。用于类层次之间的基类和派生类之间指针或者引用的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。可以将空指针转化成目标类型的空指针。可以将任何类型的表达式转化成 void 类型。不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。2.const_cast:主要用于 const 与非 const、volatile 与非 volatile 之间的转换。强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。3.reinterpret_cast:改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。reinterpret_cast 转换时,执行的过程是逐个比特复制的操作。4.dynamic_cast:其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL;不能用于基本数据类型的转换。在向上进行转换时,即派生类的指针转换成基类的指针和 static_cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。C++#include <iostream> #include <cstring> using namespace std; class Base { }; class Derive : public Base { }; int main() { Base *p1 = new Derive(); Derive *p2 = new Derive(); //向上类型转换 p1 = dynamic_cast<Base *>(p2); if (p1 == NULL) { cout << "NULL" << endl; } else { cout << "NOT NULL" << endl; //输出 } return 0; }在下行转换时,基类的指针类型转化为派生类的指针类型,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会转化成功。C++#include <iostream> #include <cstring> using namespace std; class Base { public: virtual void fun() { cout << "Base::fun()" << endl; } }; class Derive : public Base { public: virtual void fun() { cout << "Derive::fun()" << endl; } }; int main() { Base *p1 = new Derive(); Base *p2 = new Base(); Derive *p3 = new Derive(); //转换成功 p3 = dynamic_cast<Derive *>(p1); if (p3 == NULL) { cout << "NULL" << endl; } else { cout << "NOT NULL" << endl; // 输出 } //转换失败 p3 = dynamic_cast<Derive *>(p2); if (p3 == NULL) { cout << "NULL" << endl; // 输出 } else { cout << "NOT NULL" << endl; } return 0; }
C++
0
0
0
浏览量1529
菜鸟码转

16.什么是模板特化

模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。特化分为全特化和偏特化:全特化:模板中的模板参数全部特例化。偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。说明:要区分下函数重载与函数模板特化定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。实例:C++#include <iostream> #include <cstring> using namespace std; //函数模板 template <class T> bool compare(T t1, T t2) { cout << "通用版本:"; return t1 == t2; } template <> //函数模板特化 bool compare(char *t1, char *t2) { cout << "特化版本:"; return strcmp(t1, t2) == 0; } int main(int argc, char *argv[]) { char arr1[] = "hello"; char arr2[] = "abc"; cout << compare(123, 123) << endl; cout << compare(arr1, arr2) << endl; return 0; } /* 运行结果: 通用版本:1 特化版本:0 */
C++
0
0
0
浏览量2018
菜鸟码转

2.std::move() 函数的实现原理

1.std::move() 函数原型:move 函数是将任意类型的左值转为其类型的右值引用。C++template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type &&>(t); }首先需要了解一下,引用折叠原理:右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。我们已经知道折叠原理,通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。再次详细描述 move 函数的处理流程:首先利用万能模板将传入的参数 t 进行处理,我们知道右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变;对参数 t 做一次右值引用,根据引用折叠规则,右值的右值引用是右值引用,而左值的右值引用是普通的左值引用。万能模板既可以接受左值作为实参也可以接受右值作为实参。通过 remove_refrence 移除引用,得到参数 t 具体的类型 type;最后通过 static_cast<> 进行强制类型转换,返回 type && 右值引用。2.remove_reference 具体实现:remove_reference 主要作用是解除类型中引用并返回变量的实际类型。C++//原始的,最通用的版本 template <typename T> struct remove_reference{     typedef T type;  //定义 T 的类型别名为 type };   //部分版本特例化,将用于左值引用和右值引用 template <class T> struct remove_reference<T&> //左值引用 { typedef T type; }   template <class T> struct remove_reference<T&&> //右值引用 { typedef T type; }       //举例如下,下列定义的a、b、c三个变量都是int类型 int i; remove_refrence<decltype(42)>::type a;             //使用原版本, remove_refrence<decltype(i)>::type  b;             //左值引用特例版本 remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 3.forward 的实现:forward 保证了在转发时左值右值特性不会被更改,实现完美转发。主要解决引用函数参数为右值时,传进来之后有了变量名就变成了左值。比如如下代码:C++#include <iostream> using namespace std; template<typename T> void fun(T&& tmp) { cout << "fun rvalue bind:" << tmp << endl; } template<typename T> void fun(T& tmp) { cout << "fun lvalue bind:" << tmp << endl; } template<typename T> void test(T&& x) { fun(x); fun(std::forward<T>(x)); } int main() { int a = 10; test(10); test(a); return 0; } /* fun lvalue bind:10 fun rvalue bind:10 fun lvalue bind:10 fun lvalue bind:10 */参数 x 为右值,到了函数内部则变量名则变为了左值,我们使用 forward 即可保留参数 x 的属性。forward 函数实现如下:C++ /** * @brief Forward an lvalue. * @return The parameter cast to the specified type. * * This function is used to implement "perfect forwarding". */ template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); } /** * @brief Forward an rvalue. * @return The parameter cast to the specified type. * * This function is used to implement "perfect forwarding". */ template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept { static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument" " substituting _Tp is an lvalue reference type"); return static_cast<_Tp&&>(__t); }forward 函数的处理流程:forward 同样利用引用折叠的特性,对参数 t 做一次右值引用,根据引用折叠规则,右值的右值引用是右值引用,而左值的右值引用是普通的左值引用。forward 的实现有两个函数:第一个,接受的参数是左值引用,只能接受左值。第二个,接受的参数是右值引用,只能接受右值。根据引用折叠的原理:如果传递的是左值,_Tp 推断为 T &,则返回变成static_cast<T& &&>,也就是 static_cast<T&>,所以返回的是左值引用。如果传递的是右值,_Tp 推断为 T&& 或者 T,则返回变成 static_cast<T && &&>,所以返回的是右值引用。forward 与 move 最大的区别是,move 在进行类型转换时,利用 remove_reference 将外层的引用全部去掉,这样可以将 t 强制转换为指定类型的右值引用,而 forward 则利用引用折叠的技巧,巧妙的保留了变量原有的属性。以下示例代码就可以观察到 move 与 forward 的原理区别:C++#include <iostream> using namespace std; typedef int& lref; typedef int&& rref; void fun(int&& tmp) { cout << "fun rvalue bind:" << tmp << endl; } void fun(int& tmp) { cout << "fun lvalue bind:" << tmp << endl; } int main() { int a = 11; int &b = a; int &&c = 100; fun(static_cast<lref &&>(b)); fun(static_cast<rref &&>(c)); fun(static_cast<int &&>(a)); fun(static_cast<int &&>(b)); fun(static_cast<int &&>(c)); return 0; } /* fun lvalue bind:11 fun rvalue bind:100
C++
0
0
0
浏览量2022
菜鸟码转

8.迭代器的作用

迭代器:一种抽象的设计概念,在设计模式中有迭代器模式,即提供一种方法,使之能够依序寻访某个容器所含的各个元素,而无需暴露该容器的内部表述方式。迭代器只是一种概念上的抽象,具有迭代器通用功能和方法的对象都可以叫做迭代器。迭代器有很多不同的能力,可以把抽象容器和通用算法有机的统一起来。迭代器基本分为五种,输入输出迭代器,前向逆向迭代器,双向迭代器和随机迭代器。输入迭代器(Input Iterator):只能向前单步迭代元素,不允许修改由该迭代器所引用的元素;输出迭代器(Output Iterator):只能向前单步迭代元素,对由该迭代器所引用的元素只有写权限;向前迭代器(Forward Iterator):该迭代器可以在一个区间中进行读写操作,它拥有输入迭代器的所有特性和输出迭代器的部分特性,以及向前单步迭代元素的能力;双向迭代器(Bidirectional Iterator):在向前迭代器的基础上增加了向后单步迭代元素的能力;随机访问迭代器(Random Access Iterator):不仅综合以后 4 种迭代器的所有功能,还可以像指针那样进行算术计算;在 C++ STL 中,容器 vector、deque 提供随机访问迭代器,list 提供双向迭代器,set 和 map 提供向前迭代器。使用迭代器的优点:代码编写方便:迭代器提供了通用接口来遍历元素,不用担心容器的大小,使用迭代器我们可以简单地使用成员函数 end() 来判断容器的结尾,遍历内容方便而简洁;代码可重用性高::迭代器提供了一组通用的 api 访问和遍历容器中的元素。迭代器支持代码的可重用性,它们可以被使用访问任何容器的元素。容器可以动态处理:迭代器能够在需要时方便地从容器中动态添加或删除元素。程序实例:C++#include <iostream> #include <vector> using namespace std; int main() { vector<int> arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; vector<int>::iterator iter = arr.begin(); // 定义迭代器 for (; iter != arr.end(); ++iter) { cout << *iter << " "; } return 0; } /* 运行结果: 1 2 3 4 5 6 7 8 9 0 */
C++
0
0
0
浏览量680
菜鸟码转

5.常量指针和指针常量的区别

1.常量指针:常量指针本质上是个指针,只不过这个指针指向的对象是常量。特点:const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。(可以这样理解:* 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。)C++const int * p; int const * p;注意 1:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。例如:C++#include <iostream> using namespace std; int main() { const int c_var = 8; const int *p = &c_var; *p = 6; // error: assignment of read-only location '* p' return 0; }注意 2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此,可以被重新赋值。例如:C++#include <iostream> using namespace std; int main() { const int c_var1 = 8; const int c_var2 = 8; const int *p = &c_var1; p = &c_var2; return 0; }2.指针常量:指针常量的本质上是个常量,只不过这个常量的值是一个指针。特点:const 位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型,即以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。C++const int var; int * const c_p = &var; 注意 1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。C++#include <iostream> using namespace std; int main() { int var, var1; int * const c_p = &var; c_p = &var1; // error: assignment of read-only variable 'c_p' return 0; }注意 2:指针的内容可以改变。C++#include <iostream> using namespace std; int main() { int var = 3; int * const c_p = &var; *c_p = 12; return 0; }3.指向常量的指针常量:指向常量的指针常量,指针的指向不可修改,指针所指的内存区域中的值也不可修改。C++#include <iostream> using namespace std; int main() { int var, var1; const int * const c_p = &var; c_p = &var1; // error: assignment of read-only variable 'c_p' *c_p = 12; // error: assignment of read-only location '*c_p' return 0; }4.部分特例:根据前三部分的结论,我们可以得到以下代码的表示内容:C++int ** const p; // p 是一指针常量,它是一个指向指针的指针常量; int * const * p; // p 是一个指针,它是一个指向指针常量的指针; int const ** p; // p 是一个指针,它是一个指向常量的指针的指针; int * const * const p; // p 是一指针常量,它是一个指向指针常量的指针常量;
C++
0
0
0
浏览量2015

履历