C++学习笔记——从面试题出发学习C++
C++学习笔记——从面试题出发学习C++
- C++学习笔记——从面试题出发学习C++
- 1. 成员函数的重写、重载和隐藏的区别?
- 2. 构造函数可以是虚函数吗?内联函数可以是虚函数吗?析构函数为什么一定要是虚函数?
- 3. 解释左值/右值、左值/右值引用、std::move、移动语义、完美转发等相关的概念?
- 3.1 左值/右值的概念
- 3.2 左值引用/右值引用的概念
- 3.3 std::move的作用
- 3.3 移动语义的概念
- 3.4 完美转发的概念
- 4. decltype、volatile、explicit、override、mutable关键字的作用?
- 4.1 decltype
- 4.2 volatile
- 4.3 explicit
- 4.4 override
- 4.5 mutable
- 5. 构造函数相关的default和delete关键字的作用?
- 6 . extern C的作用?
- 7. 解释动态多态和静态多态的区别?
- 8. 菱形继承有什么问题,如何解决?
- 9. 段错误有哪些类型?
- 10. 如何定义一个只能在堆上(栈上)生成对象的类?
- 11. delete this 合法吗?
- 12. 不同类型智能指针的区别?
- 12.1 auto_ptr
- 12.2 shared_ptr
- 12.3 weak_ptr
- 12.4 unique_ptr
- 13. 不同强制类型转换运算符的区别?
- 13.1 static_cast
- 13.2 dynamic_cast
- 13.3 reinterpret_cast
- 13.4 const_cast
- 14. 如何重载操作符?重载操作符的返回值?流运算符为什么不能通过成员函数重载?
- 16. 如何理解函数指针、类成员函数指针?
C++学习笔记——从面试题出发学习C++
C++博大精深,在学习过程中我也有看过《Effective C++》、《Efficient C++》、《C++ Prime》这样一些C++的经典大作,但是个人感觉是由于语法太多,很难抓住重点,在工作中如果不很经常用到某个语法,即使在书籍上有看过也会很快忘记。而刷面试题是一个很好的查漏补缺的方式,本博客将以面试题为切入点,将面试题中涉及的语法展开学习以彻底搞懂,进而达到在平常的工作中能够灵活运用目的,下面就逐个开始语法的学习:
1. 成员函数的重写、重载和隐藏的区别?
这里我们直接给出三种不同概念的定义:
重载指的是同一作用域内(例如同为某一个类的成员函数),函数名相同,入参不同的情况;
隐藏指的是不同作用域内(例如两个函数分别位于父类和子类中),函数名相同,入参不同则直接构成隐藏,入参相同且非虚函数,否则为重写;
重写特指两个函数分别位于父类和子类中,函数名相同,入参相同且为虚函数的情况(注意和隐藏做区别);
我们要知道:
重载是静态多态的表达形式,重写是动态多态的表达形式。
除此之外,下面这种情况注意隐藏和重写输出的区别:
如下是隐藏,调用的是Base类中的func函数
#include<iostream>using namespace std;class Base
{
public:void fun(int i){ cout << "Base::fun(int) : " << i << endl;}
};class Derived : public Base
{
public:void fun(int i){ cout << "Derived::fun(int) : " << i << endl;}
};
int main()
{Base b;Base * pb = new Derived();pb->fun(3);//Base::fun(int)system("pause");return 0;
}
如下是重写,调用的是Derived类中的func函数:
#include<iostream>using namespace std;class Base
{
public:virtual void fun(int i){ cout << "Base::fun(int) : " << i << endl;}
};class Derived : public Base
{
public:virtual void fun(int i){ cout << "Derived::fun(int) : " << i << endl;}
};
int main()
{Base b;Base * pb = new Derived();pb->fun(3);//Derived::fun(int)system("pause");return 0;
}
2. 构造函数可以是虚函数吗?内联函数可以是虚函数吗?析构函数为什么一定要是虚函数?
首先我们要了解虚函数的基本实现原理,虚函数的基本结构是虚表:
(1)虚表是一个指针数组,每个元素对应一个虚函数的函数指针;普通函数的调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针;
(2)虚表内的虚函数的函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就已经完成构造;
(3)虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表(父类和子类属于不同的类)。为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。从下图我们可以理解类对象、虚表和虚函数的区别:

其中,类B继承类A,类C继承类B。类A有两个虚函数A::vfunc1()和A::vfunc2(),类B重写了B::vfunc1(),类C重写了C::vfunc2()。当我们使用指针调用虚函数,且满足指针向上转型条件时就可以触发动态绑定,如下代码:
int main()
{B bObject;A *p = & bObject;p->vfunc1(); // 最终调用的时B::vfunc1()这个函数
}
了解了虚函数的基本实现原理后我们来回答下上面相关的问题:
(1)构造函数不可以是虚函数,因为每个对象中的虚函数的实现依赖虚函数表指针_vptr指向的虚函数表来确定,在执行构造函数前对象尚未完成创建,虚函数表指针_vptr还不存在,也就无法通过虚函数表确定构造函数的具体实现。
(2)虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。 内联是在发生在编译期间,编译器会自主选择内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
(3)将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们创建一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
3. 解释左值/右值、左值/右值引用、std::move、移动语义、完美转发等相关的概念?
3.1 左值/右值的概念
左值是可寻址的变量,有持久性(例如变量名、返回引用的函数调用、前置自增自减、解引用等);
右值是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的(例如字面值、返回非引用类型的函数调用、后置自增自减、算数表达式等);
左值和右值主要的区别之一是左值可以被修改,而右值不能。
3.2 左值引用/右值引用的概念
左值引用:引用一个对象,其通常用在函数传参或者返回值来避免对象拷贝;
右值引用:就是必须绑定到右值的引用,通过 && 获得右值引用,其主要作用是实现移动语义和完美转发(两个概念见下文)
这里补充很重要的一点,常量左值引用也可以引用右值,通常用于入参或者类拷贝构造函数中,更全面的关系如下表所示:

3.3 std::move的作用
std::move的作用是将一个左值变成右值,换一个角度讲就是可以将一个右值引用指向左值。如下:
int main()
{int a = 1;int &&b = std::move(a);std::cout << "a = " << a << std::endl;std::cout << "b = " << b << std::endl;return 0;
}
输出结果如下:
a = 1
b = 1
更具体地,std::move的定义如下:
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
其中remove_reference的含义是获得去掉引用的参数类型,从上面定义可以看出,std::move()并不是什么黑魔法,而只是进行了简单的类型转换:
(1)如果传递的是左值,则推导为左值引用,然后由static_cast转换为右值引用
(2)如果传递的是右值,则推导为右值引用,然后由static_cast转换为右值引用
使用std::move之后,就意味着两点:
(1)原对象不再被使用,如果对其使用会造成不可预知的后果(对int等基础类型进行move()操作,不会改变其原值)
(2)所有权转移,资源的所有权被转移给新的对象
3.3 移动语义的概念
移动语义是通过移动构造函数和移动赋值构造函数实现的,其主要目的是为了避免资源的重新分配。如下面分别定义拷贝构造函数、赋值构造函数、移动构造函数、移动赋值构造函数四种构造函数:
class BigObj {
public:explicit BigObj(size_t length): length_(length), data_(new int[length]) {}// 析构函数~BigObj() {if (data_ != NULL) {delete[] data_;length_ = 0;}}// 拷贝构造函数BigObj(const BigObj& other): length_(other.length_), data(new int[other.length_]) {std::copy(other.mData, other.mData + mLength, mData);}// 赋值构造函数BigObj& operator=(const BigObj& other) {if (this != &other;) {delete[] data_; length_ = other.length_;data_ = new int[length_];std::copy(other.data_, other.data_ + length_, data_);}return *this;}// 移动构造函数BigObj(BigObj&& other) : data_(nullptr), length_(0) {data_ = other.data_;length_ = other.length_;other.data_ = nullptr;other.length_ = 0;}// 移动赋值构造函数BigObj& operator=(BigObj&& other) { if (this != &other;) {delete[] data_;data_ = other.data_;length_ = other.length_;other.data_ = nullptr;other.length_ = 0;}return *this;}private:size_t length_;int* data_;
};
在移动构造函数和移动赋值构造函数中,没有分配任何新资源,也没有复制其它资源,仅仅是将other的资源进行了移动,占为己用,而other中的内存被移动到新成员后,other中原有的内容则消失,因此需要进行重置。当data_大到百万个元素时,如果使用原来拷贝构造函数的话,就需要将该数百万元素逐个进行复制,性能可想而知。而如果使用该移动构造函数,因为不涉及到新资源的创建,不仅可以节省很多资源,而且性能也有很大的提升。
那么我们如何触发移动构造函数和移动赋值构造函数呢?也就是触发移动语义呢?可以有如下几种场景:
场景一:
int main() {std::vector<BigObj> v;v.push_back(BigObj(10));v.push_back(BigObj(20));return 0;
}
上述代码中,两个push_back()调用都将解析为push_back(T&&),push_back(T&&)使用BigObj的移动构造函数将资源从参数移动到vector的内部BigObj对象中。而在C++11之前,上述代码则生成参数的拷贝,然后调用BigObj的拷贝构造函数。
如果参数是左值,则将调用push_back(T&):
int main() {std::vector<BigObj> v;BigObj obj(10);v.push_back(obj); // 此处调用push_back(T&)return 0;
}
如果希望调用push_back(T&&),则需要使用std::move函数:
int main() {std::vector<BigObj> v;BigObj obj(10);v.push_back(std::move(obj)); // 此处调用push_back(T&&)return 0;
}
这里需要注意的是,因为调用了std::move,因此下文要避免对obj对象做进一步操作,否则可能会导致内存越界等问题。
场景二:
BigObj fun() {return BigObj();
}
BigObj obj = fun(); // C++11以前
BigObj &&obj = fun(); // C++11
上述代码中,在C++11之前,我们只能通过编译器优化(N)RVO的方式来提升性能,如果不满足编译器的优化条件,则只能通过拷贝等方式进行操作。自C++11引入右值引用后,对于不满足(N)RVO条件,也可以通过移动语义避免拷贝,进而达到优化的目的。
3.4 完美转发的概念
完美转发的定义指的是函数模板可以将自己的参数“完美”地转发给内部调用的其他函数。这里注意,首先一定是模板函数,其次完美指的是不仅能准确转发参数的值,还能保证转发参数的左、右值属性不变。这个为什么重要呢?因为在很多场景中是否完美转发,直接决定了该参数的传递过程使用的是调用拷贝构造函数还是调用移动构造函数,结合上面移动语义的概念我们应该就很好理解这个对于性能的提升是非常重要的。
首先我们定义一个没有完美转发的functoin函数:
template<typename T>
void function(T&& t) {vfun(t);
}
这里首先需要解释下T&&的含义,C++ 11规定,在模板函数中使用右值引用语法定义的参数来说,它表示“万能引用”,满足"引用折叠规则"(T& & –> T&,T&& & –> T&,T& && –>T&,T&& && –> T&&),因此左值参数最终转换后仍为左值,右值参数最终转成右值。尽管如此,因为形参t是有名字且可取地址的,因此其传递到内部后仍然是左值,仍然满足不了完美转发的定义。
因此在C++ 11中就引入了std::forward函数,std::forward的定义如下:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{return static_cast<T&&>(param);
}template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{return static_cast<T&&>(param);
}
第一个是左值引用模板函数,第二个是右值引用模板函数,其中remove_reference的含义是获得去掉引用的参数类型(左右值属性不变),根据“万能应用”的定义,因此左值参数最终转换后仍为左值,右值参数最终转成右值。因此如下实现就完成了完美转发:
template <typename T>
void function(T&& t) {vfun(forward<T>(t));
}
这里补充下,在C++ 11之前是否可以实现完美转发的效果呢?上面我们介绍了右值是通过常量左值引用传递的,因此通过重载函数模板是可以实现同样功能的,如下:
//接收右值参数
template <typename T>
void function(const T& t) {otherdef(t);
}
//接收左值参数
template <typename T>
void function(T& t) {otherdef(t);
}
但是相较之下这种实现方式会更麻烦些,因此我们通常还是使用std::forward函数实现完美转发。
4. decltype、volatile、explicit、override、mutable关键字的作用?
4.1 decltype
- decltype和auto都是用于推导变量类型,但用法不同,如下:
其中auto要求变量必须初始化,deltype不要求auto varname = value; decltype(exp) varname = value; - decltype要求推导的对象一定是有类型的,但对象可以使一个普通的变量、表达式或者其他任意复杂的形式
- decltype的经典用法是和priority_queue结合,当我们需要对一个自定义类进行堆排序时,如果不使用decltype的话写法如下:
如果换成deltype进行类型推导的话代码会显得更加简洁:bool cmp (const T& lhs, const T& rhs) { return lhs > rhs; }priority_queue<T, vector<T>, bool (*) (const T& lhs, const T& rhs)> pq(cmp);bool cmp(const T lhs, const T rhs) { return rhs < lhs; }priority_queue<T, vector<T>, decltype(cmp)> pq(cmp)
4.2 volatile
- volatile的含义是让编译器每次操作变量时一定是内存中取出,而不是使用已经存在寄存器中的值,主要使用在(1)中断服务中修改供其他程序检测的变量;(2)多任务环境下各任务间共享的标志应该加volatile(注意不是多线程);(3)存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。
- volatile可以使修饰指针,用法和const类似;
- volatile并不能解决多线程中的问题,具体原因可以参考谈谈 C/C++ 中的 volatile
4.3 explicit
-
explicit修饰单参构造函数,用于说明该构造函数不能进行隐式转换(无参或者多参构造函数无隐式转换),如下
class People{ public:int age;explicit People(int a){age=a;} };void foo(void){People p1(10); //方式一,正确People* p_p2=new People(10); //方式二,正确People p3=10; //方式三,这个会报错,如果去掉explicit的话就不会报错。}上述代码中如果没有explicit修饰的话方式三会正确运行,此时类成员age会被赋值为10,这就是C++对单参构造函数规定的一种隐式转换,这会导致一些很难被发现的bug,因此对于explicit关键字应该是能用则用。
4.4 override
-
如果父类在虚函数声明时使用了override关键字,那么该函数必须重载其子类中的同名函数,否则代码将无法通过编译。
-
override存在的目的是为了避免在重载子类同名函数过程中意外创建新的虚函数的情况,如下所示:
class Base { public:virtual void Show(int x); // 虚函数 };class Derived : public Base { public:virtual void Sh0w(int x); // o 写成了 0,新的虚函数 virtual void Show(double x); // 参数列表不一样,新的虚函数 virtual void Show(int x) const; // const 属性不一样,新的虚函数 };
4.5 mutable
-
mutable 只能用来修饰类的非静态和非常量数据成员,而被 mutable 修饰的数据成员,可以在 const 成员函数中修改。
-
在 lambda 表达式的设计中按值捕获的方式不允许程序员在 lambda 函数的函数体中修改捕获的变量。而以 mutable 修饰 lambda 函数,则可以打破这种限制,如下:
int x{0}; auto f1 = [=]() mutable {x = 42;}; // okay, 创建了一个函数类型的实例 auto f2 = [=]() {x = 42;}; // error, 不允许修改按值捕获的外部变量的值 -
在一个类中,应尽量或者不用mutable,大量使用mutable表示程序设计存在缺陷
5. 构造函数相关的default和delete关键字的作用?
要了解default和delete关键字的作用首先要知道C++对于默认构造函数的定义,在C++中,如果用户没有定义构造函数,那么编译器会自动生成一系列默认构造函数,包括拷贝构造,赋值构造,移动构造,移动赋值构造。delete的用处禁止默认构造函数的生成,如下:
myClass(const myClass&)=delete;//表示删除默认拷贝构造函数,即不能进行默认拷贝
myClass & operatir=(const myClass&)=delete;//表示删除默认拷贝构造函数,即不能进行默认拷贝
但是一旦用户定义了带参数的构造函数,那么编译器就不会再自动生成默认构造函数,此时其他构造函数都需要用户来定义。此时可以通过default关键字要求编译器来生成一个默认构造函数
myClass()=default;//表示默认存在构造函数
6 . extern C的作用?
extern “C”的作用主要是为了能够正确实现C++代码调用其他C语言代码。加上extern C后,会指示编译器这部分代码按C语言而不是C++的方式进行编译。C++和C语言在编译期一个典型的不同是,C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,例如函数 void fun(int, int) 编译后的可能是 _fun_int_in,而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,因此,如果不加 extern “C”,在链接阶段,链接器会从 moduleA 生成的目标文件 moduleA.obj 中找 _fun_int_int 这样的符号,显然这是不可能找到的
在C++出现之前,很多底层的库是C语言写的,extern “C”的存在就是为了更好的支持原来的C代码和C语言库。extern “C”的使用方法如下:
-
使用单一语句
extern “C” double sqrt(double); -
使用复合语句,相当于复合语句中的申明都加了extern “C”
extern “C” {double sqrt(double);int min(int, int); } -
包含头文件,相当于头文件中的申明都加了extern “C”
extern “C” {#include <cmath> } -
不可以将extern “C”添加在函数内部,如果函数有多个申明,可以都加extern “C”,也可以只出现在第一次申明中,后面的申明会接受第一个链接指示符的规则。
7. 解释动态多态和静态多态的区别?
动态多态的设计思想是:对于相关的对象类型,确定他们之间的一个共同功能集,然后在父类中,将这些共同的功能声明为多个公共的虚函数结构。各个子类重写这些虚函数,以完成具体的功能。用户代码通过指向父类指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。因此,我们可以总结出来动态多态的三个条件:
- 通过指针来调用函数;
- 指针向上转型(即定义一个父类指针指向子类对象);
- 调用的是虚函数;
静态多态的设计思想是:对于相关的对象类型,直接实现他们各自的定义,不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明。用户把操作这些对象的函数定义为模板,当需要操作什么类型的对象时,直接对模板制定该类型实参即可。
相较之下,动态多态更加灵活,适合更复杂的应用场景。静态多态是编译期实现的多态,效果更高,适用于对性能要求高的场景,如UI渲染等。
8. 菱形继承有什么问题,如何解决?
当Father 类和 Mother 类分别从 GrandParent 继承而来,GrandSon 从 Father 类和 Mother 类多继承而来,类似于这样的继承方式就会形成菱形结构。菱形结构主要问题是:
-
数据二义性,当GrandParent中存在类成员变量m是,GrandSon是无法直接调用的m,而必须通过域运算符(::)进行区分,例如
GrandSon grandSon; std::cout << grandSon.Mother::m << std::endl; std::cout << grandSon.Father::m << std::endl; -
空间浪费,GrandSon中会存在两份积累GrandParent的数据;
解决菱形继承的方法是虚继承,即
class Father : virtual public GrandParent {};
class Mother : virtual public GrandParent {};
使用虚继承的基类属于虚基类,可以看到,虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式声明的。程序运行时,只有最后的派生类执行对基类的构造函数调用,而忽略其他派生类对虚基类的构造函数调用。从而避免对基类数据成员重复初始化。因此,虚基类只会构造一次。
9. 段错误有哪些类型?
所谓的段错误 就是指访问的内存超出了系统所给这个程序的内存空间,这里我们粗略的进行一下分类:
- 往受到系统保护的内存地址写数据;
- 内存越界(数组越界,变量类型不一致等);
我们还可以列举一些需要注意的经常导致段错误的场景:
- 定义了指针后记得初始化,在使用的时候记得判断是否为NULL;
- 在使用数组的时候是否被初始化,数组下标是否越界,数组元素是否存在等;
- 在变量处理的时候变量的格式控制是否合理等;
定位段错误的工具通常时GDB,具体定位方式就不在此展开了。
10. 如何定义一个只能在堆上(栈上)生成对象的类?
解答这个问题我们首先要知道,生成类对象的方式一共就两种:第一种是静态建立,即通过构造函数构建栈或者静态对象;第二种是动态建立,即通过new运算符对象构建堆对象。其中new运算符是先执行operator new()函数在堆空间中搜索合适的内存,第二步是调用构造函数使用该内存进行初始化。因此new运算符其实也是间接调用了类的构造函数。
对象只能在堆上建立的类,最佳的实现方式如下:
class A
{
protected : A(){} ~A(){}
public : static A* create() { return new A(); } void destory() { delete this ; }
};
对于这种实现方式,有几点需要解释:
- 对象只能在堆上建立最直接的想法是将构造函数设置私有,但是上文也提到new运算符其实也是间接调用了构造函数,因此该方法不可取。
- 当对象在栈上建立时,编译器会析构函数来确定如何释放内存,当析构函数时私有时,编译器就无法调用析构函数来释放内存,也就不会在栈空间上为对象分配内存,因此将析构函数设置为私有即可得到一个只能在堆上建立的类。
- 考虑到类的可继承性,因此我们可以将析构函数设置为保护变量,这样这个类还是可继承的,且只能在堆上初始化。
对象只能在栈上建立的类,其实只需要将operator new()设置为私有即可:
class A
{
private : void * operator new ( size_t t){} // 注意函数的第一个参数和返回值都是固定的 void operator delete ( void * ptr){} // 重载了new就需要重载delete
public : A(){} ~A(){}
};
11. delete this 合法吗?
是合法的,但是需要注意如下几点:
- this指向的对象必须是new出来的,不能是new[] 、placement new、栈、全局或者其他方式分配的对象,只能是简单的new出来的;
- delete this 一旦被调用相当于该对象不复存在,对象下的其他成员函数或者成员对象不得再被调用,也不得以任何形式操作该对象,包括比较、打印、类型转换;
- 不能在析构函数中调用delete this,因为delete this会出发析构函数进而造成无限递归。
为了更好理解以上几点,这里我们对delete关键字功能进一步拓展,delete的过程分为如下两步:
p->~Object();
p->operator delete(p);
其中
第一步是调用p指向的Object对象的析构函数,这一步通常由用户自己定义,在析构函数中并不会对当前对象进行内存释放;
第二步是调用p对象的内存释放语句,如果用户没有实现该方法,将调用系统内存释放原语operator delete§左释放该对象内存的动作。指示这个对象消亡前最后的动作。通常用户是不需要override这个函数的,如果需要,一定要在最后调用系统的operate delete操作释放该对象所占用的内存。下面这段代码就很好地说明了这个问题:
class x {
public :x(){}~x() {printf("~x()/n");//delete p; //这里若进行此操作则会陷入嵌套}void operator delete(void * ptr) {printf("x::delete()/n");}
};void main() {x* p=new x;delete p; //依次调用p的~x()和operator deletedelete p; //不会报错,因为"operator delete" override了系统函数,没有进行::operator delete(this)操作。delete p; //同理依然不会报错
}
12. 不同类型智能指针的区别?
这是一个老生常谈的问题了,这里我们再总结下:c++中一共有四个只能指针:auto_ptr,shared_ptr,weak_ptr,unique_ptr,其中后三个是c++ 11支持的,auto_ptr已经被c++ 11弃用。
12.1 auto_ptr
- c++98的方案**,c++11已经弃用**;
- auto_ptr采用所有权模式,因此两个auto_ptr不能同时拥有一个对象。如下做法是错误的,会造成多次析构:
int*p=new int(0); auto_ptr<int>ap1(p); auto_ptr<int>ap2(p); - auto_ptr的析构函数中删除指针用得是delete,而不是delete[],因此auto_ptr不能管理数组指针;
- auto_ptr被剥夺所有权时,再次使用会报错:,如下做法是错误的:
int*p=new int(0); auto_ptr<int>ap1(p); auto_ptr<int>ap2=ap1; cout<<*ap1;//错误,此时ap1只剩一个null指针在手了
12.2 shared_ptr
- shared_ptr是用来解决所有权共享的问题,多个shared_ptr可以指向相同对象,对象的资源会在最后一个指针销毁时释放;
- shared拥有成员函数:use_count(返回引用计数个数 ),unique(返回是否独占所有权),swap(交换所拥有的对象),reset(放弃对象所有权,会引起原有对象计数减少),get(返回对象指针);
- 可以通过make_shared构造shared_ptr;
12.3 weak_ptr
-
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果两个shared_ptr相互引用,即那么这两个指针的引用计数永远不会变为零,资源永远不会被释放。
-
weak_ptr解决上述问题的原理是weak_ptr只可以从另一个shared_ptr或者weak_ptr对象构造,且weak_ptr的构造和析构不会引起引用计数的增加或者减少。
-
weak_ptr不可以直接访问对象的方法,要将weak_ptr转化为shared_ptr。如下是weak_ptr应用的典型场景:
/*修改,将share_ptr互相引用换成weak_ptr,避免死锁问题*/ class B; class A {public:weak_ptr<B> pb_;~A() {cout<<"A delete\n";} }; class B {public:weak_ptr<A> pa_;~B() {cout<<"B delete\n";} }; void fun() {shared_ptr<B> pb(new B());shared_ptr<A> pa(new A());pb->pa_ = pa;pa->pb_ = pb;cout<<pb.use_count()<<endl;cout<<pa.use_count()<<endl; } int main() {fun();return 0; }
12.4 unique_ptr
-
unique_ptr实现了独有所有权的语义。unique_ptr是仅能移动的类型,拷贝是不被允许的。当unique_ptr被移动时,资源的所有权也从源指针转移给目标指针,而源指针将被置空。如下是一些基本操作:
int main() {// 创建一个unique_ptr实例unique_ptr<int> pInt(new int(5));unique_ptr<int> pInt2(pInt); // 报错unique_ptr<int> pInt3 = pInt; // 报错unique_ptr<int> pInt4 = std::move(pInt); // 转移所有权//cout << *pInt << endl; // 报错,pInt为空cout << *pInt4 << endl;unique_ptr<int> pInt5(std::move(pInt4)); // 转移所有权 } -
可以通过make_unique构造unique_ptr;
13. 不同强制类型转换运算符的区别?
这也是C++中非常基本的一个知识点,主要有四种强制转换类型运算符:const_cast,reinterpret_cast,static_cast,dynamic_cast
13.1 static_cast
static_cast执行非动态转换,没有运行时类型检查来保证转换的安全性,通常有如下几种用法:
- 用于类层次结构中父类和子类之间指针或引用的转换,进行上行转换(把子类指针转换成父类指针)是安全的;进行下行转换(把父类指针转换成子类指针)时,由于没有动态类型检查,所以是不安全的。
- 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
- 把void指针转换成目标类型的指针,这种转换是不安全的。
- 把任何类型的表达式转换成void类型。
13.2 dynamic_cast
dynamic_cast是唯一一个在运行时进行类型检测的转换,可以使用它来检验多个动态对象,该转换一般用于含有虚函数的基类和派生类之间。dynamic_cast使用的注意事项如下:
- dynamic_cast转换符只能用于指针或者引用。
- dynamic_cast转换符只能用于含有虚函数的类。
- dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试。
13.3 reinterpret_cast
reinterpret_cast可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针),使用该操作符的危险性较高,一般不应使用该操作符。
13.4 const_cast
const_cast用来修改类型的const/volatile属性,这种类型的转换主要是用来操作所传对象的 const 属性,可以加上 const 属性,也可以去掉 const 属性。
14. 如何重载操作符?重载操作符的返回值?流运算符为什么不能通过成员函数重载?
如下是最经典的复数运算的重载操作:
#include <iostream>
using namespace std;
class Complex {
public:Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }//运算符+重载成员函数Complex operator + (const Complex &c2) const;//运算符-重载成员函数Complex operator - (const Complex &c2) const;void display() const; //输出复数
private:double real; //复数实部double imag; //复数虚部
};
例 复数类加减法运算重载为成员函数
Complex Complex::operator+(const Complex &c2) const{//创建一个临时无名对象作为返回值 return Complex(real+c2.real, imag+c2.imag);
}Complex Complex::operator-(const Complex &c2) const{//创建一个临时无名对象作为返回值return Complex(real-c2.real, imag-c2.imag);
}
具体说来:
-
重载前缀一元运算符:
如果为非静态成员函数,声明如下:return-type operator op ();如果为非成员函数,声明如下:
return-type operator op ( class-type ); -
重载后缀一元运算符(递增、递减):
当为递增或递减运算符的后缀形式指定重载运算符时,其参数的类型必须是 int;指定任何其他类型都将产生错误。 -
重载二元运算符:
如果为非静态成员函数,声明如下return-type operator op( class-type )如果为非成员函数(全局函数、友元函数),声明如下:
return-type operator op(class-type, class-type)关于如果要重载 B 为类成员函数,使之能够实现表达式 class-type1 OP class-type2,其中 class-type1为A 类对象,则 OP 应被重载为 A 类的成员函数,形参类型应该是 class-type2所属的类型。经重载后,表达式 class-type1 OP class-type2相当于 class-type1.operator OP(class-type2)
重载操作符可以以void,对象的值或者引用的形式进行返回,注意这里的对象指向的是调用对象本身(对于全局的二元运算符函数就是左侧实参,否则就是*this),以下几种情况需要返回调用对象的引用:
- 等号连续赋值
- +=,-=,*=,/=
- <<,>>
原因是:
- 允许进行连续赋值;
- 防止返回对象的时候调用拷贝构造函数和析构函数导致不必要的开销,降低赋值运算符的效率;
- 和所有内置类型和标准程序库提供的类型遵循相同的协议;
流运算符为什么不能通过成员函数重载的原因是因为通过类的成员函数重载必须是运算符的第一个是自己,对流运算的重载要求第一个参数是流对象,因此我们通常使用友元来解决问题,例如如果我们实现:
ostream & operator<<(ostream &output)
{return output;
}
这样我们只能使用data<<cout的形式调用进行调用,而如果要实现cout << data的形式,就需要以如下形式实现:
#include <iostream>
using namespace std;class Person {public:Person(string name, int age) : name(name), age(age) {}// 重载输出运算符friend ostream &operator<<(ostream &out, const Person &p) {out << p.name << " " << p.age;return out;}private:string name;int age;
};int main() {Person p("John", 25);cout << p << endl;return 0;
}
16. 如何理解函数指针、类成员函数指针?
函数指针和函数名的区别在于,他们虽然都指向了函数在内存的入口地址,但函数指针本身是个指针变量,对他做&取地址的话会拿到这个变量本身的地址去,而对函数名做&取址,得到的还是函数的入口地址。
-
函数指针的定义方式如下:
int test(int a) {return a; } int main(int argc, const char * argv[]) {int (*fp)(int a);fp = test;cout<<fp(2)<<endl;return 0; }函数指针所指向的函数一定要报纸函数的返回值类型、函数参数个数和类型一致,我们还可以使用typedef来可以简化函数指针的定义:
int test(int a) {return a; }int main(int argc, const char * argv[]) {typedef int (*fp)(int a);fp f = test;cout<<f(2)<<endl;return 0; } -
函数指针同样可以作为参数传递给函数,还可以构建函数指针数组,这些是我们实现回调函数的基础:
int test(int a) {return a-1; } int test2(int (*fun)(int),int b) {int c = fun(10)+b;return c; }int main(int argc, const char * argv[]) {typedef int (*fp)(int a);fp f = test;cout<<test2(f, 1)<<endl; // 调用 test2 的时候,把test函数的地址作为参数传递给了 test2return 0; }构成函数指针数字的方式如下:
void t1(){cout<<"test1"<<endl;} void t2(){cout<<"test2"<<endl;} void t3(){cout<<"test3"<<endl;}int main(int argc, const char * argv[]) {typedef void (*fp)(void);fp b[] = {t1,t2,t3}; // b[] 为一个指向函数的指针数组b[0](); // 利用指向函数的指针数组进行下标操作就可以进行函数的间接调用了return 0; }
对于类成员函数指针,和函数指针的区别主要如下:
-
对于指向类成员函数的函数指针,引用时必须传入一个类对象的this指针,所以必须由类实体调用,即使用 . (实例对象)或者 ->*(实例对象指针)调用类成员函数指针所指向的函数*。
-
对于虚函数, 其地址在编译时期是未知的,所以对于虚成员函数取其地址(子类父类都是如此),所能获得的只是一个索引值,即其在虚函数表的偏移位置。
-
对于静态成员函数,和非静态成员函数的变脸的赋值方式是一样的,都是&ClassName::memberVariable形式,但是其声明方式不同:
class A{ public://p1是一个指向非static成员函数的函数指针void (A::*p1)(void);//p2是一个指向static成员函数的函数指针void (*p2)(void);A(){p1 =&A::funa; //函数指针赋值一定要使用 &p2 =&A::funb;}void funa(void){puts("A");}static void funb(void){puts("B");} };类成员函数指针就不仅仅是类成员函数的内存起始地址,还需要能解决因为 C++ 的多重继承、虚继承而带来的类实例地址的调整问题,所以类成员函数指针在调用的时候一定要传入类实例对象。因此上面类的调用方法如下:
int main() { // 非静态和静态类成员函数指针调用方式对比A a;void (A::*pa)(void);pa = &A::funa; (a.*pa)(); //打印 AA *b = &a;(b->*pa)(); //打印 Avoid (*pb)(void);pb = &A::funb;pb(); //打印 Breturn 0; }我们也可以使用在类中定义好的类成员变量调用:
int main() {A a;// p是指向A中非static成员函数的函数指针void (A::*p)(void);(a.*a.p1)(); //打印 A,这个看起来或许有些奇怪// 使用.*(实例对象)或者->*(实例对象指针)调用类成员函数指针所指向的函数p = a.p1;(a.*p)(); //打印 AA *b = &a;(b->*p)(); //打印 Ap = a.p2; //error,尽管a.p2本身是个非static变量,但是a.p2是指向static函数的函数指针 }
相关文章:
C++学习笔记——从面试题出发学习C++
C学习笔记——从面试题出发学习C C学习笔记——从面试题出发学习C1. 成员函数的重写、重载和隐藏的区别?2. 构造函数可以是虚函数吗?内联函数可以是虚函数吗?析构函数为什么一定要是虚函数?3. 解释左值/右值、左值/右值引用、std:…...
WebAPIs 第二天
DOM事件基础 事件监听事件类型事件对象 一.事件监听 ① 概念:就是让程序检测是否有事件发生,一旦有事件触发,就立即调用一个函数做出响应,也成为绑定事件或者注册事件 ② 语法:元素对象.addEventListener(事件类型&…...
解决macOS执行fastboot找不到设备的问题
背景 最近准备给我的备用机Redmi Note 11 5G刷个类原生的三方ROM,MIUI实在是用腻了。搜罗了一番,在XDA上找到了一个基于Pixel Experience开发的ROM:PixelExperience Plus for Redmi Note 11T/11S 5G/11 5G/POCO M4 Pro 5G (everpal)…...
Linux命令 -- chmod
Linux命令 -- chmod 参数含义权限说明修改文件权限修改目录权限 参数含义 文件用户 u 文件所有者g 文件所有者同组的用户o 其它用户a 所有用户 文件权限 r 读权限(对应数值4)w 写权限(对应数值2)x 执行权限(对应数…...
国产超低功耗32位MCU的应用
随着物联网技术的不断发展,超低功耗MCU已经成为了物联网方案中主要的芯片处理技术。超低功耗MCU具有众多的优点,其中一大所用就是能够大大提高物联网设备的续航能力,保证设备在长时间内不掉电不断电。那么,超低功耗MCU在物联网方案…...
将数组(矩阵)旋转根据指定的旋转角度scipy库的rotate方法
【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 将数组(矩阵)旋转 根据指定的旋转角度 scipy库的rotate方法 关于下列代码说法正确的是? import numpy as np from scipy.ndimage import rotate a np.array([[1,2,3,4], …...
MFC创建和使用OCX控件
文章目录 MFC建立OCX控件注册OCX控件与反注册使用Internet Explorer测试ocx控件OCX控件添加方法OCX控件添加事件Web使用OCX控件MFC使用OCX控件使用OCX控件调用ocx的功能函数对ocx的事件响应OCX控件调试工具tstcon32.exe加载ocx控件使用tstcon32.exe调试ocxMFC建立OCX控件 新建…...
【设计模式】抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在抽象工厂模式中,接口是负责创建一个相关对象…...
小白带你学习linux的Redis3.2集群(三十三)
目录 一、Redis主从复制 1、概念 2、作用 3、缺点 4、流程 5、搭建 6、验证 二、Reids哨兵模式 1、概念 2、作用 3、缺点 4、结构 5、搭建 6、验证 三、Redis集群 1、概述 2、原理 3、架构细节 4、选举过程 四、搭建 1、第一步现在外部使用finalshell 9.9…...
嵌入式技术,就在你的手边!
嵌入式技术,听起来多么高大上的名词,同时它也确实是当今信息技术的前沿领域,但这并不意味着它就距离我们很遥远。 事实恰恰相反,在当今科技发展迅猛的时代,嵌入式技术成为了人们生活中不可或缺的一部分。它以其小巧、高…...
nodejs+vue+elementui健康饮食美食菜谱分享网站系统
本系统采用了nodejs语言的vue框架,数据采用MySQL数据库进行存储。结合B/S结构进行开发设计,功能强大,界面化操作便于上手。本系统具有良好的易用性和安全性,系统功能齐全,可以满足饮食分享管理的相关工作。 语言 node.…...
input 设置type=“number“,鼠标悬停关闭提示语
一、问题 最近刚发现input 设置type"number"之后,鼠标悬停会出现提示语:请输入有效值。两个最接近的有效值分别为xx和xx。想要输入的值确实为number格式,又可以输入小数,不限制小数位,所以要把这讨厌的提示去…...
CSDN互利共赢玩法实战!!!
csdn项目第一波基本都顺利跑了起来,我们总计找来了一两千个新的项目源码,来让大家变现。 在实战中,主要两个玩法,一个引流,一个付费资源。付费资源门槛越来越高,所以我们这一波升级完成的号,就非…...
java.sql.SQLFeatureNotSupportedException 问题及可能的解决方法
目录 问题 分析: 解决方法 问题 java.sql.SQLFeatureNotSupportedException 分析: 可能是你的 druid的maven依赖版本太低了,我的以前是1.1.16,就出现了异常! 解决方法 把druid的maven依赖版本调高! 运…...
如何在 .NET Core WebApi 中处理 MultipartFormDataContent 中的文件
问题描述# 上图示例展示了用户通过 IOS 客户端发送请求时,对应后端接口接收到的 Request 内容。从请求内容的整体结果,我们可以看出这是一个 multipart/form-data 的数据格式,由于这种数据是由多个 multipart section 组成,所以我…...
【智力悬疑题】——【“找凶手”解法】
“找凶手”题目解法 “案件题目”💻 某地发生了一起凶杀案,警察通过排查确定杀人凶手必为4个嫌疑犯中的一个。以下为4个嫌疑犯的供词: A说:不是我。 B说:是C。 C说:是D。 D说:C在胡说。 已知3个…...
【论文阅读】基于深度学习的时序异常检测——TimesNet
系列文章链接 参考数据集讲解:数据基础:多维时序数据集简介 论文一:2022 Anomaly Transformer:异常分数预测 论文二:2022 TransAD:异常分数预测 论文三:2023 TimesNet:基于卷积的多任…...
P3741 honoka的键盘
题目背景 honoka 有一个只有两个键的键盘。 题目描述 一天,她打出了一个只有这两个字符的字符串。当这个字符串里含有 VK 这个字符串的时候,honoka 就特别喜欢这个字符串。所以,她想改变至多一个字符(或者不做任何改变…...
编写第一个 React Native 程序
React Native 目录 使用React Native CLI命令创建的目录如下图所示: 重要目录说明 目录说明__tests__存放测试用例的目录.bundle / config配置文件(一般不会用到)android 和 IOS 文件夹这两个文件夹主要是存放安卓和 ios 相关的配置文件和…...
AI:03-基于深度神经网络的低空无人机目标检测图像识别的研究
文章目录 数据集收集与预处理深度神经网络模型设计模型训练与优化目标检测与图像识别代码实现:实验结果与分析讨论与展望低空无人机的广泛应用为许多领域带来了巨大的潜力和机会。为了实现无人机的自主导航和任务执行,准确的目标检测和图像识别是至关重要的。本文旨在研究并提…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...
练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
