1.说说include头文件的顺序以及双引号""和尖括号<>的区
区别:
(1)尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件。
(2)编译器预处理阶段查找头文件的路径不一样。
查找路径:
(1)使用尖括号<>的头文件的查找路径:编译器设置的头文件路径-->系统变量。
(2)使用双引号""的头文件的查找路径:当前头文件目录-->编译器设置的头文件路径-->系统变量。简述C++从代码到可执行二进制文件的过程
2.简述C++从代码到可执行二进制文件的过程
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
-
预编译:这个过程主要的处理操作如下:
(1) 将所有的#define删除,并且展开所有的宏定义
(2) 处理所有的条件预编译指令,如#if、#ifdef
(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
(4) 过滤所有的注释
(5) 添加行号和文件名标识。
-
编译:这个过程主要的处理操作如下:
(1) 词法分析:将源代码的字符序列分割成一系列的记号。
(2) 语法分析:对记号进行语法分析,产生语法树。
(3) 语义分析:判断表达式是否有意义。
(4) 代码优化:
(5) 目标代码生成:生成汇编代码。
(6) 目标代码优化:
-
汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
-
链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。
链接分为静态链接和动态链接。
静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。
而动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
1.1.8 说说 static关键字的作用
参考回答
-
定义全局静态变量和局部静态变量:在变量前面加上static关键字。初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。只不过全局静态变量和局部静态变量的作用域不一样;bss段存放未初始化的变量
data段存放已初始化的变量 -
定义静态函数:在函数返回类型前加上static关键字,函数即被定义为静态函数。静态函数只能在本源文件中使用;
-
在变量类型前加上static关键字,变量即被定义为静态变量。静态变量只能在本源文件中使用;
//示例 static int a; static void func();
-
在c++中,static关键字可以用于定义类中的静态成员变量:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间。
-
在c++中,static关键字可以用于定义类中的静态成员函数:与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间。
1.1.13 说说什么是野指针,怎么产生的,如何避免?
参考回答
-
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
-
产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
-
避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针
1.1.18 说说const和define的区别。
参考回答
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
-
const生效于编译的阶段;define生效于预处理阶段。
-
const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
-
const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
1.1.19 说说C++中函数指针和指针函数的区别。
指针函数与函数指针的用法与区别_程序员小明丶的博客-CSDN博客_函数指针与指针函数的区别
1.1.24 简述const(星号)和(星号)const的区别
参考回答
//const* 是常量指针,*const 是指针常量 int const *a; //a指针所指向的内存里的值不变,即(*a)不变 int *const a; //a指针所指向的内存地址不变,即a不变
1.2.1 简述一下堆和栈的区别
参考回答
区别:
-
堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。
-
堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
-
堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。
1.2.2 简述C++的内存管理
C++语言中的内存模型_ITSOK_U的博客-CSDN博客_c++内存模型
参考回答
-
内存分配方式:
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
堆,就是那些由new分配的内存块,一般一个new就要对应一个delete。
自由存储区,就是那些由malloc等分配的内存块,和堆是十分相似的,不过是用free来结束自己的生命。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中
常量存储区,这是一块比较特殊的存储区,里面存放的是常量,不允许修改。
-
常见的内存错误及其对策:
(1)内存分配未成功,却使用了它。
(2)内存分配虽然成功,但是尚未初始化就引用它。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
(5)释放了内存却继续使用它。
对策:
(1)定义指针时,先初始化为NULL。
(2)用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
(3)不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
(4)避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
(5)动态内存的申请与释放必须配对,防止内存泄漏
(6)用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
(7)使用智能指针。
1.3.2 简述一下面向对象的三大特征
参考回答
面向对象的三大特征是封装、继承、多态。
-
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
-
继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
三种继承方式
继承方式 private继承 protected继承 public继承 基类的private成员 不可见 不可见 不可见 基类的protected成员 变为private成员 仍为protected成员 仍为protected成员 基类的public成员 变为private成员 变为protected成员 仍为public成员仍为public成员 -
多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。
1.3.3 简述一下 C++ 的重载和重写,以及它们的区别
参考回答
-
重写
是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
示例如下:
#include<bits/stdc++.h> using namespace std; class A { public: virtual void fun() { cout << "A"; } }; class B :public A { public: virtual void fun() { cout << "B"; } }; int main(void) { A* a = new B(); a->fun();//输出B,A类中的fun在B类中重写 }
-
重载
我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值其中包括(int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来加以区分。这样的代码不美观而且给程序猿也带来了很多的不便。于是在C++中人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
#include<bits/stdc++.h> using namespace std; class A { void fun() {}; void fun(int i) {}; void fun(int i, int j) {}; void fun1(int i,int j){}; };
1.3.4 说说 C++ 的重载和重写是如何实现的
参考答案
-
C++利用命名倾轧(name mangling)技术,来改名函数名,区分参数不同的同名函数。命名倾轧是在编译阶段完成的。
C++定义同名重载函数:
#include<iostream> using namespace std; int func(int a,double b) { return ((a)+(b)); } int func(double a,float b) { return ((a)+(b)); } int func(float a,int b) { return ((a)+(b)); } int main() { return 0; }
由上图可得,d代表double,f代表float,i代表int,加上参数首字母以区分同名函数。
-
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
-
用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
-
存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
-
多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
-
重写用虚函数来实现,结合动态绑定。
-
纯虚函数是虚函数再加上 = 0。
-
抽象类是指包括至少一个纯虚函数的类。
纯虚函数:virtual void fun()=0。即抽象类必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。
-
1.3.9 说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
参考答案
-
创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);
-
如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)
-
基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;
-
成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
-
派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)
-
综上可以得出,初始化顺序:
父类构造函数–>成员类对象构造函数–>自身构造函数
其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。
析构顺序和构造顺序相反。
1.3.10 简述下向上转型和向下转型C++ 向上与向下类型转换(static_cast与dynamic_cast)_十一月zz的博客-CSDN博客_c++向下类型转换
1.3.12 简述一下 C++ 中的多态
由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。 多态分为静态多态和动态多态:
-
静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。
比如一个简单的加法函数:
include<iostream> using namespace std; int Add(int a,int b)//1 { return a+b; } char Add(char a,char b)//2 { return a+b; } int main() { cout<<Add(666,888)<<endl;//1 cout<<Add('1','2');//2 return 0; }
显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信你可以将顺序调过来试试。
-
动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
-
虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
-
通过基类类型的指针或引用来调用虚函数。
说到这,得插播一条概念:重写——也就是基类中有一个虚函数,而在派生类中也要重写一个原型(返回值、名字、参数)都相同的虚函数。不过协变例外。协变是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。
//协变测试函数 #include<iostream> using namespace std; class Base { public: virtual Base* FunTest() { cout << "victory" << endl; return this; } }; class Derived :public Base { public: virtual Derived* FunTest() { cout << "yeah" << endl; return this; } }; int main() { Base b; Derived d; b.FunTest(); d.FunTest(); return 0; }
-
1.3.13 说说为什么要虚析构,为什么不能虚构造
-
虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。
-
用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
-
用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。
-
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数
2.不能虚构造
-
从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论)
-
从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
-
从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。
1.3.14 说说模板类是在什么时候实现的
-
模板实例化:模板的实例化分为显示实例化和隐式实例化,前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的
-
模板具体化:当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。
-
代码示例:
#include <iostream> using namespace std; // #1 模板定义 template<class T> struct TemplateStruct { TemplateStruct() { cout << sizeof(T) << endl; } }; // #2 模板显示实例化 template struct TemplateStruct<int>; // #3 模板具体化 template<> struct TemplateStruct<double> { TemplateStruct() { cout << "--8--" << endl; } }; int main() { TemplateStruct<int> intStruct; TemplateStruct<double> doubleStruct; // #4 模板隐式实例化 TemplateStruct<char> llStruct; }
运行结果:
4 --8-- 1
1.3.26 请问拷贝构造函数的参数是什么传递方式,为什么
参考回答
-
拷贝构造函数的参数必须使用引用传递
-
如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。
需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。
1.3.34 C++ 中哪些函数不能被声明为虚函数?
参考回答
常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
-
为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
-
为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)
构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数
-
为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数)
内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数
-
为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。
静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别
-
为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
1.3.35 解释下 C++ 中类模板和模板类的区别
参考回答
-
类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
-
模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。
答案解析
-
类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template <class T1,class T2>class someclass{…};在定义对象时分别代入实际的类型名,如 someclass<int,double> obj;
-
和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
-
模板可以有层次,一个类模板可以作为基类,派生出派生模板类。
1.4.5 STL 容器用过哪些,查找的时间复杂度是多少,为什么?
参考回答
STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set等。容器底层实现方式及时间复杂度分别如下:
-
vector
采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N)
-
deque
采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N)
-
list
采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
插入: O(1)
查看: O(N)
删除: O(1)
-
map、set、multimap、multiset
上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
插入: O(logN)
查看: O(logN)
删除: O(logN)
-
unordered_map、unordered_set、unordered_multimap、 unordered_multiset
上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N)
查看: O(1),最坏情况O(N)
删除: O(1),最坏情况O(N)
注意:容器的时间复杂度取决于其底层实现方式。
迭代器实例
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2); v.push_back(3); v.push_back(4);
vector<int>::const_iterator i; //常量迭代器
for (i = v.begin(); i != v.end(); ++i) //v.begin()表示v第一个元素迭代器指针,++i指向下一个元素
cout << *i << ","; //*i表示迭代器指向的元素
cout << endl;
vector<int>::reverse_iterator r; //反向迭代器
for (r = v.rbegin(); r != v.rend(); r++)
cout << *r << ","; cout << endl;
vector<int>::iterator j; //非常量迭代器
for (j = v.begin();j != v.end();j++)
*j = 100;
for (i = v.begin();i != v.end();i++)
cout << *i << ",";
return 0;
} /* 运行结果: 1,2,3,4, 4,3,2,1, 100,100,100,100, */
1.4.8 说说 STL 迭代器是怎么删除元素的
C++中利用迭代器删除元素会发生什么?_YF_Li123的博客-CSDN博客_利用迭代器删除元素会发生什么
1.4.9 说说 STL 中 resize 和 reserve 的区别
STLvector中reserve()和resize()区别_daidaihaha的博客-CSDN博客
1.5.1C++11新特性
// decltype 用法举例
int a = 0;
decltype(a) b = 1; //b 被推导成了 int
decltype(10.8) x = 5.5; //x 被推导成了double
decltype(x + 100) y; //y 被推导成了 double
Lambda匿名函数
C++中Lambda函数(匿名函数)_大道一支菜鸟的博客-CSDN博客_c++ lambda函数
智能指针
C++ STL 四种智能指针_恋喵大鲤鱼的博客-CSDN博客_智能指针
智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。
unique_ptr
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露,例如,以new创建对象后因为发生异常而忘记调用delete时的情形特别有用。
//程序实例
unique_ptr<string> pu1(new string("hello world"));
unique_ptr<string> pu2;
//pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string("You")); // #2 allowed
shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。
weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
#include <vector>
#include <iostream>
#include<algorithm>
#include<assert.h>
#include <string>
using namespace std;
int main()
{
shared_ptr<int> sp(new int(10));
assert(sp.use_count() == 1);
weak_ptr<int> wp(sp); // 从 shared_ptr 创建 weak_ptr
assert(wp.use_count() == 1);
if (!wp.expired()) { // 判断 weak_ptr 观察的对象是否失效
shared_ptr<int> sp2 = wp.lock(); // 获得一个 shared_ptr
*sp2 = 100;
assert(wp.use_count() == 2);
}
assert(wp.use_count() == 1);
cout << "int:" << *sp << endl;
return 0;
}